From 2ace936a951c6562145b4dd359e104eca629bd05 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 23:32:13 +0200 Subject: [PATCH 001/160] Draft to refactor fusion to new publisher and use as FusionHost --- openpype/hosts/fusion/api/__init__.py | 27 +-- openpype/hosts/fusion/api/menu.py | 2 +- openpype/hosts/fusion/api/pipeline.py | 174 ++++++++++++------ openpype/hosts/fusion/api/workio.py | 45 ----- .../deploy/MenuScripts/openpype_menu.py | 4 +- .../fusion/plugins/create/create_exr_saver.py | 44 +++-- 6 files changed, 160 insertions(+), 136 deletions(-) delete mode 100644 openpype/hosts/fusion/api/workio.py diff --git a/openpype/hosts/fusion/api/__init__.py b/openpype/hosts/fusion/api/__init__.py index ed70dbca50..495fe286d5 100644 --- a/openpype/hosts/fusion/api/__init__.py +++ b/openpype/hosts/fusion/api/__init__.py @@ -1,20 +1,11 @@ from .pipeline import ( - install, - uninstall, - + FusionHost, ls, imprint_container, - parse_container -) - -from .workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root + parse_container, + list_instances, + remove_instance ) from .lib import ( @@ -30,21 +21,11 @@ from .menu import launch_openpype_menu __all__ = [ # pipeline - "install", - "uninstall", "ls", "imprint_container", "parse_container", - # workio - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root", - # lib "maintained_selection", "update_frame_range", diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 7a6293807f..4e415cafba 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -144,7 +144,7 @@ class OpenPypeMenu(QtWidgets.QWidget): host_tools.show_creator() def on_publish_clicked(self): - host_tools.show_publish() + host_tools.show_publisher() def on_load_clicked(self): host_tools.show_loader(use_context=True) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index c92d072ef7..b73759fee0 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -3,6 +3,7 @@ Basic avalon integration """ import os import logging +import contextlib import pyblish.api @@ -14,15 +15,14 @@ from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, register_inventory_action_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, - deregister_inventory_action_path, AVALON_CONTAINER_ID, ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.fusion import FUSION_HOST_DIR +from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher from openpype.tools.utils import host_tools + from .lib import ( get_current_comp, comp_lock_and_undo_chunk, @@ -47,71 +47,99 @@ class CompLogHandler(logging.Handler): comp.Print(entry) -def install(): - """Install fusion-specific functionality of OpenPype. +class FusionHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): + name = "fusion" - This is where you install menus and register families, data - and loaders into fusion. + def install(self): + """Install fusion-specific functionality of OpenPype. - It is called automatically when installing via - `openpype.pipeline.install_host(openpype.hosts.fusion.api)` + This is where you install menus and register families, data + and loaders into fusion. - See the Maya equivalent for inspiration on how to implement this. + It is called automatically when installing via + `openpype.pipeline.install_host(openpype.hosts.fusion.api)` - """ - # Remove all handlers associated with the root logger object, because - # that one always logs as "warnings" incorrectly. - for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) + See the Maya equivalent for inspiration on how to implement this. - # Attach default logging handler that prints to active comp - logger = logging.getLogger() - formatter = logging.Formatter(fmt="%(message)s\n") - handler = CompLogHandler() - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(logging.DEBUG) + """ + # Remove all handlers associated with the root logger object, because + # that one always logs as "warnings" incorrectly. + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) - pyblish.api.register_host("fusion") - pyblish.api.register_plugin_path(PUBLISH_PATH) - log.info("Registering Fusion plug-ins..") + # Attach default logging handler that prints to active comp + logger = logging.getLogger() + formatter = logging.Formatter(fmt="%(message)s\n") + handler = CompLogHandler() + handler.setFormatter(formatter) + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) - register_inventory_action_path(INVENTORY_PATH) + pyblish.api.register_host("fusion") + pyblish.api.register_plugin_path(PUBLISH_PATH) + log.info("Registering Fusion plug-ins..") - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled - ) + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + register_inventory_action_path(INVENTORY_PATH) - # Fusion integration currently does not attach to direct callbacks of - # the application. So we use workfile callbacks to allow similar behavior - # on save and open - register_event_callback("workfile.open.after", on_after_open) + pyblish.api.register_callback("instanceToggled", + on_pyblish_instance_toggled) + # Fusion integration currently does not attach to direct callbacks of + # the application. So we use workfile callbacks to allow similar + # behavior on save and open + register_event_callback("workfile.open.after", on_after_open) -def uninstall(): - """Uninstall all that was installed + # region workfile io api + def has_unsaved_changes(self): + comp = get_current_comp() + return comp.GetAttrs()["COMPB_Modified"] - This is where you undo everything that was done in `install()`. - That means, removing menus, deregistering families and data - and everything. It should be as though `install()` was never run, - because odds are calling this function means the user is interested - in re-installing shortly afterwards. If, for example, he has been - modifying the menu or registered families. + def get_workfile_extensions(self): + return [".comp"] - """ - pyblish.api.deregister_host("fusion") - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering Fusion plug-ins..") + def save_workfile(self, dst_path=None): + comp = get_current_comp() + comp.Save(dst_path) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) - deregister_inventory_action_path(INVENTORY_PATH) + def open_workfile(self, filepath): + # Hack to get fusion, see + # openpype.hosts.fusion.api.pipeline.get_current_comp() + fusion = getattr(sys.modules["__main__"], "fusion", None) - pyblish.api.deregister_callback( - "instanceToggled", on_pyblish_instance_toggled - ) + return fusion.LoadComp(filepath) + + def get_current_workfile(self): + comp = get_current_comp() + current_filepath = comp.GetAttrs()["COMPS_FileName"] + if not current_filepath: + return None + + return current_filepath + + def work_root(self, session): + work_dir = session["AVALON_WORKDIR"] + scene_dir = session.get("AVALON_SCENEDIR") + if scene_dir: + return os.path.join(work_dir, scene_dir) + else: + return work_dir + # endregion + + @contextlib.contextmanager + def maintained_selection(self): + from .lib import maintained_selection + return maintained_selection() + + def get_containers(self): + return ls() + + def update_context_data(self, data, changes): + print(data, changes) + + def get_context_data(self): + return {} def on_pyblish_instance_toggled(instance, old_value, new_value): @@ -254,3 +282,43 @@ def parse_container(tool): return container +def list_instances(creator_id=None): + """Return created instances in current workfile which will be published. + + Returns: + (list) of dictionaries matching instances format + """ + + comp = get_current_comp() + tools = comp.GetToolList(False, "Loader").values() + + instance_signature = { + "id": "pyblish.avalon.instance", + "identifier": creator_id + } + instances = [] + for tool in tools: + + data = tool.GetData('openpype') + if not isinstance(data, dict): + return + + if creator_id and data.get("identifier") != creator_id: + continue + + if data.get("id") != instance_signature["id"]: + continue + + instances.append(tool) + + return instances + + +def remove_instance(instance): + """Remove instance from current workfile. + + Args: + instance (dict): instance representation from subsetmanager model + """ + # Assume instance is a Fusion tool directly + instance.Delete() diff --git a/openpype/hosts/fusion/api/workio.py b/openpype/hosts/fusion/api/workio.py deleted file mode 100644 index 939b2ff4be..0000000000 --- a/openpype/hosts/fusion/api/workio.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Host API required Work Files tool""" -import sys -import os - -from .lib import get_current_comp - - -def file_extensions(): - return [".comp"] - - -def has_unsaved_changes(): - comp = get_current_comp() - return comp.GetAttrs()["COMPB_Modified"] - - -def save_file(filepath): - comp = get_current_comp() - comp.Save(filepath) - - -def open_file(filepath): - # Hack to get fusion, see - # openpype.hosts.fusion.api.pipeline.get_current_comp() - fusion = getattr(sys.modules["__main__"], "fusion", None) - - return fusion.LoadComp(filepath) - - -def current_file(): - comp = get_current_comp() - current_filepath = comp.GetAttrs()["COMPS_FileName"] - if not current_filepath: - return None - - return current_filepath - - -def work_root(session): - work_dir = session["AVALON_WORKDIR"] - scene_dir = session.get("AVALON_SCENEDIR") - if scene_dir: - return os.path.join(work_dir, scene_dir) - else: - return work_dir diff --git a/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py index 2918c552c8..685e58d58f 100644 --- a/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py +++ b/openpype/hosts/fusion/deploy/MenuScripts/openpype_menu.py @@ -13,11 +13,11 @@ def main(env): # However the contents of that folder can conflict with Qt library dlls # so we make sure to move out of it to avoid DLL Load Failed errors. os.chdir("..") - from openpype.hosts.fusion import api + from openpype.hosts.fusion.api import FusionHost from openpype.hosts.fusion.api import menu # activate resolve from pype - install_host(api) + install_host(FusionHost()) log = Logger.get_logger(__name__) log.info(f"Registered host: {registered_host()}") diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 6d93fe710a..74cd1cbea5 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,24 +1,29 @@ import os -from openpype.pipeline import ( - LegacyCreator, - legacy_io -) from openpype.hosts.fusion.api import ( get_current_comp, - comp_lock_and_undo_chunk + comp_lock_and_undo_chunk, + remove_instance, + list_instances +) + +from openpype.pipeline import ( + legacy_io, + Creator, + CreatedInstance ) -class CreateOpenEXRSaver(LegacyCreator): - +class CreateOpenEXRSaver(Creator): + identifier = "io.openpype.creators.fusion.saver" name = "openexrDefault" label = "Create OpenEXR Saver" - hosts = ["fusion"] family = "render" - defaults = ["Main"] + default_variants = ["Main"] - def process(self): + selected_nodes = [] + + def create(self, subset_name, instance_data, pre_create_data): file_format = "OpenEXRFormat" @@ -26,13 +31,13 @@ class CreateOpenEXRSaver(LegacyCreator): workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - filename = "{}..exr".format(self.name) + filename = "{}..exr".format(subset_name) filepath = os.path.join(workdir, "render", filename) with comp_lock_and_undo_chunk(comp): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) - saver.SetAttrs({"TOOLS_Name": self.name}) + saver.SetAttrs({"TOOLS_Name": subset_name}) # Setting input attributes is different from basic attributes # Not confused with "MainInputAttributes" which @@ -47,3 +52,18 @@ class CreateOpenEXRSaver(LegacyCreator): # Set file format attributes saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 + + def collect_instances(self): + for instance in list_instances(creator_id=self.identifier): + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + print(update_list) + + def remove_instances(self, instances): + for instance in instances: + remove_instance(instance) + + def get_pre_create_attr_defs(self): + return [] \ No newline at end of file From 13cb1f4bc0756eae2cf1cf7d07726e137a03a37f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 20 Sep 2022 23:37:32 +0200 Subject: [PATCH 002/160] Ensure newline --- openpype/hosts/fusion/plugins/create/create_exr_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 74cd1cbea5..4809832cd0 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -66,4 +66,4 @@ class CreateOpenEXRSaver(Creator): remove_instance(instance) def get_pre_create_attr_defs(self): - return [] \ No newline at end of file + return [] From 218c836026fcb99dd157fa56e6dfabc8bf3a8271 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 09:12:41 +0200 Subject: [PATCH 003/160] Reorder logic - makes more sense to check id first --- openpype/hosts/fusion/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index b73759fee0..f5900a2dde 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -303,10 +303,10 @@ def list_instances(creator_id=None): if not isinstance(data, dict): return - if creator_id and data.get("identifier") != creator_id: + if data.get("id") != instance_signature["id"]: continue - if data.get("id") != instance_signature["id"]: + if creator_id and data.get("identifier") != creator_id: continue instances.append(tool) From 1cf86a68f7b1961118f127cadc068e6b37e62d77 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 14:19:56 +0200 Subject: [PATCH 004/160] Remove creator in menu in favor of new publisher --- openpype/hosts/fusion/api/menu.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 4e415cafba..ec8747298c 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -52,7 +52,6 @@ class OpenPypeMenu(QtWidgets.QWidget): asset_label.setAlignment(QtCore.Qt.AlignHCenter) workfiles_btn = QtWidgets.QPushButton("Workfiles...", self) - create_btn = QtWidgets.QPushButton("Create...", self) publish_btn = QtWidgets.QPushButton("Publish...", self) load_btn = QtWidgets.QPushButton("Load...", self) manager_btn = QtWidgets.QPushButton("Manage...", self) @@ -75,7 +74,6 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addSpacing(20) - layout.addWidget(create_btn) layout.addWidget(load_btn) layout.addWidget(publish_btn) layout.addWidget(manager_btn) @@ -100,7 +98,6 @@ class OpenPypeMenu(QtWidgets.QWidget): self.asset_label = asset_label workfiles_btn.clicked.connect(self.on_workfile_clicked) - create_btn.clicked.connect(self.on_create_clicked) publish_btn.clicked.connect(self.on_publish_clicked) load_btn.clicked.connect(self.on_load_clicked) manager_btn.clicked.connect(self.on_manager_clicked) @@ -140,9 +137,6 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_workfile_clicked(self): host_tools.show_workfiles() - def on_create_clicked(self): - host_tools.show_creator() - def on_publish_clicked(self): host_tools.show_publisher() From ae5c565ab61609a3affcecc7b398ee9d0a6a874f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 14:20:50 +0200 Subject: [PATCH 005/160] Fix logic - add comments that these will remain unused however --- openpype/hosts/fusion/api/pipeline.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index f5900a2dde..1587381b1a 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -282,6 +282,7 @@ def parse_container(tool): return container +# TODO: Function below is currently unused prototypes def list_instances(creator_id=None): """Return created instances in current workfile which will be published. @@ -290,7 +291,7 @@ def list_instances(creator_id=None): """ comp = get_current_comp() - tools = comp.GetToolList(False, "Loader").values() + tools = comp.GetToolList(False).values() instance_signature = { "id": "pyblish.avalon.instance", @@ -301,7 +302,7 @@ def list_instances(creator_id=None): data = tool.GetData('openpype') if not isinstance(data, dict): - return + continue if data.get("id") != instance_signature["id"]: continue @@ -314,6 +315,7 @@ def list_instances(creator_id=None): return instances +# TODO: Function below is currently unused prototypes def remove_instance(instance): """Remove instance from current workfile. @@ -321,4 +323,4 @@ def remove_instance(instance): instance (dict): instance representation from subsetmanager model """ # Assume instance is a Fusion tool directly - instance.Delete() + instance["tool"].Delete() From d62e1eef82bbb84902a96d1d48fa26538f9a2f81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 14:21:28 +0200 Subject: [PATCH 006/160] Continue refactor to new publisher --- .../fusion/plugins/create/create_exr_saver.py | 74 ++++++++++++++++++- .../plugins/publish/collect_instances.py | 48 ++++-------- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index 4809832cd0..c2adb48bac 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -1,5 +1,7 @@ import os +import qtawesome + from openpype.hosts.fusion.api import ( get_current_comp, comp_lock_and_undo_chunk, @@ -19,7 +21,9 @@ class CreateOpenEXRSaver(Creator): name = "openexrDefault" label = "Create OpenEXR Saver" family = "render" - default_variants = ["Main"] + default_variants = ["Main"] + + description = "Fusion Saver to generate EXR image sequence" selected_nodes = [] @@ -27,6 +31,10 @@ class CreateOpenEXRSaver(Creator): file_format = "OpenEXRFormat" + print(subset_name) + print(instance_data) + print(pre_create_data) + comp = get_current_comp() workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) @@ -53,17 +61,79 @@ class CreateOpenEXRSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 + # Save all data in a "openpype.{key}" = value data + for key, value in instance_data.items(): + saver.SetData("openpype.{}".format(key), value) + def collect_instances(self): - for instance in list_instances(creator_id=self.identifier): + + comp = get_current_comp() + tools = comp.GetToolList(False, "Saver").values() + + # Allow regular non-managed savers to also be picked up + project = legacy_io.Session["AVALON_PROJECT"] + asset = legacy_io.Session["AVALON_ASSET"] + task = legacy_io.Session["AVALON_TASK"] + + for tool in tools: + + path = tool["Clip"][comp.TIME_UNDEFINED] + fname = os.path.basename(path) + fname, _ext = os.path.splitext(fname) + subset = fname.rstrip(".") + + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + variant = subset[len("render"):] + + # TODO: this should not be done this way - this should actually + # get the data as stored on the tool explicitly (however) + # that would disallow any 'regular saver' to be collected + # unless the instance data is stored on it to begin with + instance = { + # Required data + "project": project, + "asset": asset, + "subset": subset, + "task": task, + "variant": variant, + "active": not passthrough, + "family": self.family, + + # Fusion data + "tool_name": tool.Name + } + + # Use the explicit data on the saver (if any) + data = tool.GetData("openpype") + if data: + instance.update(data) + + # Add instance created_instance = CreatedInstance.from_existing(instance, self) + + # TODO: move this to lifetime data or alike + # (Doing this before CreatedInstance.from_existing wouldn't + # work because `tool` isn't JSON serializable) + created_instance["tool"] = tool + self._add_instance_to_context(created_instance) + def get_icon(self): + return qtawesome.icon("fa.eye", color="white") + def update_instances(self, update_list): + # TODO: Not sure what to do here? print(update_list) def remove_instances(self, instances): for instance in instances: + + # Remove the tool from the scene remove_instance(instance) + # Remove the collected CreatedInstance to remove from UI directly + self._remove_instance_from_context(instance) + def get_pre_create_attr_defs(self): return [] diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index fe60b83827..e42e7b5f70 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -30,7 +30,7 @@ class CollectInstances(pyblish.api.ContextPlugin): """ order = pyblish.api.CollectorOrder - label = "Collect Instances" + label = "Collect Instances Data" hosts = ["fusion"] def process(self, context): @@ -39,67 +39,47 @@ class CollectInstances(pyblish.api.ContextPlugin): from openpype.hosts.fusion.api.lib import get_frame_path comp = context.data["currentComp"] - - # Get all savers in the comp - tools = comp.GetToolList(False).values() - savers = [tool for tool in tools if tool.ID == "Saver"] - start, end, global_start, global_end = get_comp_render_range(comp) context.data["frameStart"] = int(start) context.data["frameEnd"] = int(end) context.data["frameStartHandle"] = int(global_start) context.data["frameEndHandle"] = int(global_end) - for tool in savers: + # Comp tools by name + tools = {tool.Name: tool for tool in comp.GetToolList(False).values()} + + for instance in context: + + tool_name = instance.data["tool_name"] + tool = tools[tool_name] + path = tool["Clip"][comp.TIME_UNDEFINED] - - tool_attrs = tool.GetAttrs() - active = not tool_attrs["TOOLB_PassThrough"] - - if not path: - self.log.warning("Skipping saver because it " - "has no path set: {}".format(tool.Name)) - continue - filename = os.path.basename(path) head, padding, tail = get_frame_path(filename) ext = os.path.splitext(path)[1] assert tail == ext, ("Tail does not match %s" % ext) - subset = head.rstrip("_. ") # subset is head of the filename # Include start and end render frame in label + subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) - - instance = context.create_instance(subset) instance.data.update({ - "asset": os.environ["AVALON_ASSET"], # todo: not a constant - "subset": subset, "path": path, "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant + "ext": ext, # todo: should be redundant? "label": label, + # todo: Allow custom frame range per instance "frameStart": context.data["frameStart"], "frameEnd": context.data["frameEnd"], "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], "families": ["render", "review"], - "family": "render", - "active": active, - "publish": active # backwards compatibility + "family": "render" }) + # Add tool itself as member instance.append(tool) self.log.info("Found: \"%s\" " % path) - - # Sort/grouped by family (preserving local index) - context[:] = sorted(context, key=self.sort_by_family) - - return context - - def sort_by_family(self, instance): - """Sort by family""" - return instance.data.get("families", instance.data.get("family")) From 33cf1c3089cfa91601e9a10afc8df926ef5db95a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 19:57:45 +0200 Subject: [PATCH 007/160] Cleanup and fixes --- .../fusion/plugins/create/create_exr_saver.py | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_exr_saver.py index c2adb48bac..e0366c6532 100644 --- a/openpype/hosts/fusion/plugins/create/create_exr_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_exr_saver.py @@ -4,9 +4,7 @@ import qtawesome from openpype.hosts.fusion.api import ( get_current_comp, - comp_lock_and_undo_chunk, - remove_instance, - list_instances + comp_lock_and_undo_chunk ) from openpype.pipeline import ( @@ -16,25 +14,20 @@ from openpype.pipeline import ( ) -class CreateOpenEXRSaver(Creator): +class CreateSaver(Creator): identifier = "io.openpype.creators.fusion.saver" - name = "openexrDefault" - label = "Create OpenEXR Saver" + name = "saver" + label = "Create Saver" family = "render" default_variants = ["Main"] - description = "Fusion Saver to generate EXR image sequence" - - selected_nodes = [] + description = "Fusion Saver to generate image sequence" def create(self, subset_name, instance_data, pre_create_data): + # TODO: Add pre_create attributes to choose file format? file_format = "OpenEXRFormat" - print(subset_name) - print(instance_data) - print(pre_create_data) - comp = get_current_comp() workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) @@ -61,9 +54,7 @@ class CreateOpenEXRSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 - # Save all data in a "openpype.{key}" = value data - for key, value in instance_data.items(): - saver.SetData("openpype.{}".format(key), value) + self._imprint(saver, instance_data) def collect_instances(self): @@ -112,28 +103,42 @@ class CreateOpenEXRSaver(Creator): # Add instance created_instance = CreatedInstance.from_existing(instance, self) - # TODO: move this to lifetime data or alike - # (Doing this before CreatedInstance.from_existing wouldn't - # work because `tool` isn't JSON serializable) - created_instance["tool"] = tool - self._add_instance_to_context(created_instance) def get_icon(self): return qtawesome.icon("fa.eye", color="white") def update_instances(self, update_list): - # TODO: Not sure what to do here? - print(update_list) + for update in update_list: + instance = update.instance + changes = update.changes + tool = self._get_instance_tool(instance) + self._imprint(tool, changes) def remove_instances(self, instances): for instance in instances: - # Remove the tool from the scene - remove_instance(instance) + tool = self._get_instance_tool(instance) + if tool: + tool.Delete() # Remove the collected CreatedInstance to remove from UI directly self._remove_instance_from_context(instance) - def get_pre_create_attr_defs(self): - return [] + def _imprint(self, tool, data): + + # Save all data in a "openpype.{key}" = value data + for key, value in data.items(): + tool.SetData("openpype.{}".format(key), value) + + def _get_instance_tool(self, instance): + # finds tool name of instance in currently active comp + # TODO: assign `tool` as some sort of lifetime data or alike so that + # the actual tool can be retrieved in current session. We can't store + # it in the instance itself since instance needs to be serializable + comp = get_current_comp() + tool_name = instance["tool_name"] + print(tool_name) + return { + tool.Name: tool for tool in comp.GetToolList(False).values() + }.get(tool_name) From 450a471c42a13d670eaf96a4323c92629c0b20a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 19:58:42 +0200 Subject: [PATCH 008/160] Rename `create_exr_saver.py` -> `create_saver.py` --- .../plugins/create/{create_exr_saver.py => create_saver.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/fusion/plugins/create/{create_exr_saver.py => create_saver.py} (100%) diff --git a/openpype/hosts/fusion/plugins/create/create_exr_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py similarity index 100% rename from openpype/hosts/fusion/plugins/create/create_exr_saver.py rename to openpype/hosts/fusion/plugins/create/create_saver.py From 7a046dd446bef43606317df47c487b7809701a81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 20:02:58 +0200 Subject: [PATCH 009/160] Register the saver directly on create --- openpype/hosts/fusion/plugins/create/create_saver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index e0366c6532..a0ab1c1fcf 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -56,6 +56,16 @@ class CreateSaver(Creator): self._imprint(saver, instance_data) + # Register the CreatedInstance + instance = CreatedInstance( + family=self.family, + subset_name=subset_name, + instance_data=instance_data, + creator=self) + self._add_instance_to_context(instance) + + return instance + def collect_instances(self): comp = get_current_comp() From cd0825756e753de983c651413affdcaad110120a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 20:04:17 +0200 Subject: [PATCH 010/160] Fix keyword --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index a0ab1c1fcf..c2c9ad1cb7 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -60,7 +60,7 @@ class CreateSaver(Creator): instance = CreatedInstance( family=self.family, subset_name=subset_name, - instance_data=instance_data, + data=instance_data, creator=self) self._add_instance_to_context(instance) From 01167e84caf8e1e047ecffc93d7350b34d8ada46 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:11:05 +0200 Subject: [PATCH 011/160] Barebones refactor of validators to raise PublishValidationError --- .../publish/validate_background_depth.py | 7 +++- .../plugins/publish/validate_comp_saved.py | 7 +++- .../publish/validate_create_folder_checked.py | 6 ++- .../validate_filename_has_extension.py | 4 +- .../publish/validate_saver_has_input.py | 8 +++- .../publish/validate_saver_passthrough.py | 7 ++-- .../publish/validate_unique_subsets.py | 41 ++++++++++++++----- 7 files changed, 57 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 4268fab528..f057989535 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -1,6 +1,7 @@ import pyblish.api from openpype.pipeline.publish import RepairAction +from openpype.pipeline import PublishValidationError class ValidateBackgroundDepth(pyblish.api.InstancePlugin): @@ -29,8 +30,10 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found %i nodes which are not set to float32" - % len(invalid)) + raise PublishValidationError( + "Found {} Backgrounds tools which" + " are not set to float32".format(len(invalid)), + title=self.label) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py index cabe65af6e..748047e8cf 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py +++ b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py @@ -1,6 +1,7 @@ import os import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateFusionCompSaved(pyblish.api.ContextPlugin): @@ -19,10 +20,12 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin): filename = attrs["COMPS_FileName"] if not filename: - raise RuntimeError("Comp is not saved.") + raise PublishValidationError("Comp is not saved.", + title=self.label) if not os.path.exists(filename): - raise RuntimeError("Comp file does not exist: %s" % filename) + raise PublishValidationError( + "Comp file does not exist: %s" % filename, title=self.label) if attrs["COMPB_Modified"]: self.log.warning("Comp is modified. Save your comp to ensure your " diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index f6beefefc1..3674b33644 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -1,6 +1,7 @@ import pyblish.api from openpype.pipeline.publish import RepairAction +from openpype.pipeline import PublishValidationError class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): @@ -31,8 +32,9 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found Saver with Create Folder During " - "Render checked off") + raise PublishValidationError( + "Found Saver with Create Folder During Render checked off", + title=self.label) @classmethod def repair(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 4795a2aa05..22f1db809c 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -1,6 +1,7 @@ import os import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): @@ -20,7 +21,8 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Found Saver without an extension") + raise PublishValidationError("Found Saver without an extension", + title=self.label) @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index 7243b44a3e..8d961525f0 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateSaverHasInput(pyblish.api.InstancePlugin): @@ -25,5 +26,8 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Saver has no incoming connection: " - "{} ({})".format(instance, invalid[0].Name)) + saver_name = invalid[0].Name + raise PublishValidationError( + "Saver has no incoming connection: {} ({})".format(instance, + saver_name), + title=self.label) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index aed3835de3..c191d6669c 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -1,4 +1,5 @@ import pyblish.api +from openpype.pipeline import PublishValidationError class ValidateSaverPassthrough(pyblish.api.ContextPlugin): @@ -27,8 +28,8 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): if invalid_instances: self.log.info("Reset pyblish to collect your current scene state, " "that should fix error.") - raise RuntimeError("Invalid instances: " - "{0}".format(invalid_instances)) + raise PublishValidationError( + "Invalid instances: {0}".format(invalid_instances)) def is_invalid(self, instance): @@ -36,7 +37,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): attr = saver.GetAttrs() active = not attr["TOOLB_PassThrough"] - if active != instance.data["publish"]: + if active != instance.data.get("publish", True): self.log.info("Saver has different passthrough state than " "Pyblish: {} ({})".format(instance, saver.Name)) return [saver] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index b218a311ba..b78f185a3a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -1,7 +1,10 @@ +from collections import defaultdict + import pyblish.api +from openpype.pipeline import PublishValidationError -class ValidateUniqueSubsets(pyblish.api.InstancePlugin): +class ValidateUniqueSubsets(pyblish.api.ContextPlugin): """Ensure all instances have a unique subset name""" order = pyblish.api.ValidatorOrder @@ -10,20 +13,36 @@ class ValidateUniqueSubsets(pyblish.api.InstancePlugin): hosts = ["fusion"] @classmethod - def get_invalid(cls, instance): + def get_invalid(cls, context): - context = instance.context - subset = instance.data["subset"] - for other_instance in context: - if other_instance == instance: - continue + # Collect instances per subset per asset + instances_per_subset_asset = defaultdict(lambda: defaultdict(list)) + for instance in context: + asset = instance.data.get("asset", context.data.get("asset")) + subset = instance.data.get("subset", context.data.get("subset")) + instances_per_subset_asset[asset][subset].append(instance) - if other_instance.data["subset"] == subset: - return [instance] # current instance is invalid + # Find which asset + subset combination has more than one instance + # Those are considered invalid because they'd integrate to the same + # destination. + invalid = [] + for asset, instances_per_subset in instances_per_subset_asset.items(): + for subset, instances in instances_per_subset.items(): + if len(instances) > 1: + cls.log.warning( + "{asset} > {subset} used by more than " + "one instance: {instances}".format( + asset=asset, + subset=subset, + instances=instances + )) + invalid.extend(instances) - return [] + return invalid def process(self, instance): invalid = self.get_invalid(instance) if invalid: - raise RuntimeError("Animation content is invalid. See log.") + raise PublishValidationError("Multiple instances are set to " + "the same asset > subset.", + title=self.label) From ce954651182b2f4c526f981fcd5c086989f23b36 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:17:31 +0200 Subject: [PATCH 012/160] Fix bugs in Creator --- .../fusion/plugins/create/create_saver.py | 154 +++++++++++------- 1 file changed, 99 insertions(+), 55 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index c2c9ad1cb7..347aaaf497 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -29,20 +29,12 @@ class CreateSaver(Creator): file_format = "OpenEXRFormat" comp = get_current_comp() - - workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - - filename = "{}..exr".format(subset_name) - filepath = os.path.join(workdir, "render", filename) - with comp_lock_and_undo_chunk(comp): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) - saver.SetAttrs({"TOOLS_Name": subset_name}) - # Setting input attributes is different from basic attributes - # Not confused with "MainInputAttributes" which - saver["Clip"] = filepath + self._update_tool_with_data(saver, data=instance_data) + saver["OutputFormat"] = file_format # Check file format settings are available @@ -54,6 +46,9 @@ class CreateSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 + # Fusion data for the instance data + instance_data["tool_name"] = saver.Name + self._imprint(saver, instance_data) # Register the CreatedInstance @@ -70,49 +65,17 @@ class CreateSaver(Creator): comp = get_current_comp() tools = comp.GetToolList(False, "Saver").values() - - # Allow regular non-managed savers to also be picked up - project = legacy_io.Session["AVALON_PROJECT"] - asset = legacy_io.Session["AVALON_ASSET"] - task = legacy_io.Session["AVALON_TASK"] - for tool in tools: - path = tool["Clip"][comp.TIME_UNDEFINED] - fname = os.path.basename(path) - fname, _ext = os.path.splitext(fname) - subset = fname.rstrip(".") + data = self.get_managed_tool_data(tool) + if not data: + data = self._collect_unmanaged_saver(tool) - attrs = tool.GetAttrs() - passthrough = attrs["TOOLB_PassThrough"] - variant = subset[len("render"):] - - # TODO: this should not be done this way - this should actually - # get the data as stored on the tool explicitly (however) - # that would disallow any 'regular saver' to be collected - # unless the instance data is stored on it to begin with - instance = { - # Required data - "project": project, - "asset": asset, - "subset": subset, - "task": task, - "variant": variant, - "active": not passthrough, - "family": self.family, - - # Fusion data - "tool_name": tool.Name - } - - # Use the explicit data on the saver (if any) - data = tool.GetData("openpype") - if data: - instance.update(data) + # Collect non-stored data + data["tool_name"] = tool.Name # Add instance - created_instance = CreatedInstance.from_existing(instance, self) - + created_instance = CreatedInstance.from_existing(data, self) self._add_instance_to_context(created_instance) def get_icon(self): @@ -121,9 +84,18 @@ class CreateSaver(Creator): def update_instances(self, update_list): for update in update_list: instance = update.instance - changes = update.changes + + # Get the new values after the changes by key, ignore old value + new_data = { + key: new for key, (_old, new) in update.changes.items() + } + tool = self._get_instance_tool(instance) - self._imprint(tool, changes) + self._update_tool_with_data(tool, new_data) + self._imprint(tool, new_data) + + # Ensure tool name is up-to-date + instance["tool_name"] = tool.Name def remove_instances(self, instances): for instance in instances: @@ -136,19 +108,91 @@ class CreateSaver(Creator): self._remove_instance_from_context(instance) def _imprint(self, tool, data): - # Save all data in a "openpype.{key}" = value data for key, value in data.items(): tool.SetData("openpype.{}".format(key), value) def _get_instance_tool(self, instance): # finds tool name of instance in currently active comp - # TODO: assign `tool` as some sort of lifetime data or alike so that - # the actual tool can be retrieved in current session. We can't store - # it in the instance itself since instance needs to be serializable + # TODO: assign `tool` as 'lifetime' data instead of name so the + # tool can be retrieved in current session. We can't store currently + # in the CreatedInstance data because it needs to be serializable comp = get_current_comp() tool_name = instance["tool_name"] - print(tool_name) return { tool.Name: tool for tool in comp.GetToolList(False).values() }.get(tool_name) + + def _update_tool_with_data(self, tool, data): + """Update tool node name and output path based on subset data""" + if "subset" not in data: + return + + original_subset = tool.GetData("openpype.subset") + subset = data["subset"] + if original_subset != subset: + # Subset change detected + # Update output filepath + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) + filename = "{}..exr".format(subset) + filepath = os.path.join(workdir, "render", subset, filename) + tool["Clip"] = filepath + + # Rename tool + if tool.Name != subset: + print(f"Renaming {tool.Name} -> {subset}") + tool.SetAttrs({"TOOLS_Name": subset}) + + def _collect_unmanaged_saver(self, tool): + + # TODO: this should not be done this way - this should actually + # get the data as stored on the tool explicitly (however) + # that would disallow any 'regular saver' to be collected + # unless the instance data is stored on it to begin with + + print("Collecting unmanaged saver..") + comp = tool.Comp() + + # Allow regular non-managed savers to also be picked up + project = legacy_io.Session["AVALON_PROJECT"] + asset = legacy_io.Session["AVALON_ASSET"] + task = legacy_io.Session["AVALON_TASK"] + + path = tool["Clip"][comp.TIME_UNDEFINED] + fname = os.path.basename(path) + fname, _ext = os.path.splitext(fname) + subset = fname.rstrip(".") + + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + variant = subset[len("render"):] + return { + # Required data + "project": project, + "asset": asset, + "subset": subset, + "task": task, + "variant": variant, + "active": not passthrough, + "family": self.family, + + # Unique identifier for instance and this creator + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier + } + + def get_managed_tool_data(self, tool): + """Return data of the tool if it matches creator identifier""" + data = tool.GetData('openpype') + if not isinstance(data, dict): + return + + required = { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier + } + for key, value in required.items(): + if key not in data or data[key] != value: + return + + return data From af8662c87525012a8c7fd9915dcc1f2ce725c967 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:24:28 +0200 Subject: [PATCH 013/160] Fix refactor to ContextPlugin --- .../hosts/fusion/plugins/publish/validate_unique_subsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index b78f185a3a..5f0f93f764 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -40,8 +40,8 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): return invalid - def process(self, instance): - invalid = self.get_invalid(instance) + def process(self, context): + invalid = self.get_invalid(context) if invalid: raise PublishValidationError("Multiple instances are set to " "the same asset > subset.", From fe857b84429cda78235084d16c32ba3e58701aba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:26:03 +0200 Subject: [PATCH 014/160] Add title to error --- .../hosts/fusion/plugins/publish/validate_saver_passthrough.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index c191d6669c..bbafd8949e 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -29,7 +29,8 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): self.log.info("Reset pyblish to collect your current scene state, " "that should fix error.") raise PublishValidationError( - "Invalid instances: {0}".format(invalid_instances)) + "Invalid instances: {0}".format(invalid_instances), + title=self.label) def is_invalid(self, instance): From 6abfabed4057292419059fa193bf5ffcbc8b9d21 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 21 Sep 2022 23:33:13 +0200 Subject: [PATCH 015/160] Fix missing import --- openpype/hosts/fusion/api/pipeline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 1587381b1a..6fc3902949 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -2,6 +2,7 @@ Basic avalon integration """ import os +import sys import logging import contextlib From bdfe2414583480c06f0ee35113b0deea3fced1c6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:31:35 +0200 Subject: [PATCH 016/160] Refactor new publish logic to make use of "transientData" on the Creator --- .../fusion/plugins/create/create_saver.py | 33 +++++++------------ .../plugins/publish/collect_instances.py | 11 +++---- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 347aaaf497..b3a912c56a 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -46,9 +46,6 @@ class CreateSaver(Creator): saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other saver[file_format]["SaveAlpha"] = 0 - # Fusion data for the instance data - instance_data["tool_name"] = saver.Name - self._imprint(saver, instance_data) # Register the CreatedInstance @@ -57,6 +54,10 @@ class CreateSaver(Creator): subset_name=subset_name, data=instance_data, creator=self) + + # Insert the transient data + instance.transient_data["tool"] = saver + self._add_instance_to_context(instance) return instance @@ -71,11 +72,12 @@ class CreateSaver(Creator): if not data: data = self._collect_unmanaged_saver(tool) - # Collect non-stored data - data["tool_name"] = tool.Name - # Add instance created_instance = CreatedInstance.from_existing(data, self) + + # Collect transient data + created_instance.transient_data["tool"] = tool + self._add_instance_to_context(created_instance) def get_icon(self): @@ -90,17 +92,15 @@ class CreateSaver(Creator): key: new for key, (_old, new) in update.changes.items() } - tool = self._get_instance_tool(instance) + tool = instance.transient_data["tool"] self._update_tool_with_data(tool, new_data) self._imprint(tool, new_data) - # Ensure tool name is up-to-date - instance["tool_name"] = tool.Name - def remove_instances(self, instances): for instance in instances: # Remove the tool from the scene - tool = self._get_instance_tool(instance) + + tool = instance.transient_data["tool"] if tool: tool.Delete() @@ -112,17 +112,6 @@ class CreateSaver(Creator): for key, value in data.items(): tool.SetData("openpype.{}".format(key), value) - def _get_instance_tool(self, instance): - # finds tool name of instance in currently active comp - # TODO: assign `tool` as 'lifetime' data instead of name so the - # tool can be retrieved in current session. We can't store currently - # in the CreatedInstance data because it needs to be serializable - comp = get_current_comp() - tool_name = instance["tool_name"] - return { - tool.Name: tool for tool in comp.GetToolList(False).values() - }.get(tool_name) - def _update_tool_with_data(self, tool, data): """Update tool node name and output path based on subset data""" if "subset" not in data: diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index e42e7b5f70..2f3e82fded 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -45,13 +45,9 @@ class CollectInstances(pyblish.api.ContextPlugin): context.data["frameStartHandle"] = int(global_start) context.data["frameEndHandle"] = int(global_end) - # Comp tools by name - tools = {tool.Name: tool for tool in comp.GetToolList(False).values()} - for instance in context: - tool_name = instance.data["tool_name"] - tool = tools[tool_name] + tool = instance.data["transientData"]["tool"] path = tool["Clip"][comp.TIME_UNDEFINED] filename = os.path.basename(path) @@ -76,7 +72,10 @@ class CollectInstances(pyblish.api.ContextPlugin): "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], "families": ["render", "review"], - "family": "render" + "family": "render", + + # Backwards compatibility: embed tool in instance.data + "tool": tool }) # Add tool itself as member From 87621c14f5d47de295ae476879c00fbbe7efcf14 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:34:47 +0200 Subject: [PATCH 017/160] Refactor `INewPublisher` to `IPublishHost` --- openpype/hosts/fusion/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 6fc3902949..3e30c5eaf5 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -20,7 +20,7 @@ from openpype.pipeline import ( ) from openpype.pipeline.load import any_outdated_containers from openpype.hosts.fusion import FUSION_HOST_DIR -from openpype.host import HostBase, IWorkfileHost, ILoadHost, INewPublisher +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from openpype.tools.utils import host_tools @@ -48,7 +48,7 @@ class CompLogHandler(logging.Handler): comp.Print(entry) -class FusionHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): +class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "fusion" def install(self): From 11bd9e8bb1f5a62c501f4fe4388c79fe82d8bc8a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:44:05 +0200 Subject: [PATCH 018/160] Add Select Invalid Action --- openpype/hosts/fusion/api/action.py | 54 +++++++++++++++++++ .../publish/validate_saver_has_input.py | 3 ++ 2 files changed, 57 insertions(+) create mode 100644 openpype/hosts/fusion/api/action.py diff --git a/openpype/hosts/fusion/api/action.py b/openpype/hosts/fusion/api/action.py new file mode 100644 index 0000000000..1750920950 --- /dev/null +++ b/openpype/hosts/fusion/api/action.py @@ -0,0 +1,54 @@ +import pyblish.api + + +from openpype.hosts.fusion.api.lib import get_current_comp +from openpype.pipeline.publish import get_errored_instances_from_context + + +class SelectInvalidAction(pyblish.api.Action): + """Select invalid nodes in Maya when plug-in failed. + + To retrieve the invalid nodes this assumes a static `get_invalid()` + method is available on the plugin. + + """ + label = "Select invalid" + on = "failed" # This action is only available on a failed plug-in + icon = "search" # Icon from Awesome Icon + + def process(self, context, plugin): + errored_instances = get_errored_instances_from_context(context) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(errored_instances, plugin) + + # Get the invalid nodes for the plug-ins + self.log.info("Finding invalid nodes..") + invalid = list() + for instance in instances: + invalid_nodes = plugin.get_invalid(instance) + if invalid_nodes: + if isinstance(invalid_nodes, (list, tuple)): + invalid.extend(invalid_nodes) + else: + self.log.warning("Plug-in returned to be invalid, " + "but has no selectable nodes.") + + if not invalid: + # Assume relevant comp is current comp and clear selection + self.log.info("No invalid tools found.") + comp = get_current_comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + return + + # Assume a single comp + first_tool = invalid[0] + comp = first_tool.Comp() + flow = comp.CurrentFrame.FlowView + flow.Select() # No args equals clearing selection + names = set() + for tool in invalid: + flow.Select(tool, True) + names.add(tool.Name) + self.log.info("Selecting invalid tools: %s" % ", ".join(sorted(names))) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index 8d961525f0..e02125f531 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -1,6 +1,8 @@ import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateSaverHasInput(pyblish.api.InstancePlugin): """Validate saver has incoming connection @@ -13,6 +15,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): label = "Validate Saver Has Input" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] @classmethod def get_invalid(cls, instance): From 0f1ed036231d91ba6f8105d85b0c6e341e5b6487 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 23 Sep 2022 17:47:19 +0200 Subject: [PATCH 019/160] Add Select Invalid action to more validators --- .../fusion/plugins/publish/validate_background_depth.py | 4 ++++ .../plugins/publish/validate_create_folder_checked.py | 3 +++ .../plugins/publish/validate_filename_has_extension.py | 3 +++ .../fusion/plugins/publish/validate_saver_passthrough.py | 3 +++ .../hosts/fusion/plugins/publish/validate_unique_subsets.py | 6 ++++++ 5 files changed, 19 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index f057989535..261533de01 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -3,6 +3,8 @@ import pyblish.api from openpype.pipeline.publish import RepairAction from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateBackgroundDepth(pyblish.api.InstancePlugin): """Validate if all Background tool are set to float32 bit""" @@ -14,6 +16,8 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): families = ["render"] optional = True + actions = [SelectInvalidAction] + @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 3674b33644..ba943abacb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -3,6 +3,8 @@ import pyblish.api from openpype.pipeline.publish import RepairAction from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): """Valid if all savers have the input attribute CreateDir checked on @@ -16,6 +18,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): label = "Validate Create Folder Checked" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] @classmethod def get_invalid(cls, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 22f1db809c..bbba2dde6e 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -3,6 +3,8 @@ import os import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): """Ensure the Saver has an extension in the filename path @@ -17,6 +19,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): label = "Validate Filename Has Extension" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] def process(self, instance): invalid = self.get_invalid(instance) diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index bbafd8949e..56f2e7e6b8 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -1,6 +1,8 @@ import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateSaverPassthrough(pyblish.api.ContextPlugin): """Validate saver passthrough is similar to Pyblish publish state""" @@ -9,6 +11,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): label = "Validate Saver Passthrough" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] def process(self, context): diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5f0f93f764..28bab59949 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -3,6 +3,8 @@ from collections import defaultdict import pyblish.api from openpype.pipeline import PublishValidationError +from openpype.hosts.fusion.api.action import SelectInvalidAction + class ValidateUniqueSubsets(pyblish.api.ContextPlugin): """Ensure all instances have a unique subset name""" @@ -11,6 +13,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): label = "Validate Unique Subsets" families = ["render"] hosts = ["fusion"] + actions = [SelectInvalidAction] @classmethod def get_invalid(cls, context): @@ -38,6 +41,9 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): )) invalid.extend(instances) + # Return tools for the invalid instances so they can be selected + invalid = [instance.data["tool"] for instance in invalid] + return invalid def process(self, context): From a64a551fa19a472c4efe36528051394866f37cf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 10:56:34 +0200 Subject: [PATCH 020/160] Tweak label --- openpype/hosts/fusion/plugins/create/create_saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index b3a912c56a..99aa7583f1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -17,7 +17,7 @@ from openpype.pipeline import ( class CreateSaver(Creator): identifier = "io.openpype.creators.fusion.saver" name = "saver" - label = "Create Saver" + label = "Saver" family = "render" default_variants = ["Main"] From 85cb398b6ee14b66ece629fb446c97ee9fd2347a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 10:59:41 +0200 Subject: [PATCH 021/160] Implement draft for Create Workfile --- .../fusion/plugins/create/create_workfile.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/create/create_workfile.py diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py new file mode 100644 index 0000000000..26a73abb64 --- /dev/null +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -0,0 +1,129 @@ +import collections + +import qtawesome + +import openpype.hosts.fusion.api as api +from openpype.client import get_asset_by_name +from openpype.pipeline import ( + AutoCreator, + CreatedInstance, + legacy_io, +) + + +def flatten_dict(d, parent_key=None, separator="."): + items = [] + for key, v in d.items(): + new_key = parent_key + separator + key if parent_key else key + if isinstance(v, collections.MutableMapping): + items.extend(flatten_dict(v, new_key, separator=separator).items()) + else: + items.append((new_key, v)) + return dict(items) + + +class FusionWorkfileCreator(AutoCreator): + identifier = "workfile" + family = "workfile" + label = "Workfile" + + default_variant = "Main" + + create_allow_context_change = False + + data_key = "openpype.workfile" + + def collect_instances(self): + + comp = api.get_current_comp() + data = comp.GetData(self.data_key) + if not data: + return + + instance = CreatedInstance( + family=self.family, + subset_name=data["subset"], + data=data, + creator=self + ) + instance.transient_data["comp"] = comp + instance.transient_data["tool"] = None + + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + for update in update_list: + instance = update.instance + comp = instance.transient_data["comp"] + if not hasattr(comp, "SetData"): + # Comp is not alive anymore, likely closed by the user + self.log.error("Workfile comp not found for existing instance." + " Comp might have been closed in the meantime.") + continue + + # TODO: It appears sometimes this could be 'nested' + # Get the new values after the changes by key, ignore old value + new_data = { + key: new for key, (_old, new) in update.changes.items() + } + self._imprint(comp, new_data) + + def create(self, options=None): + + comp = api.get_current_comp() + if not comp: + self.log.error("Unable to find current comp") + return + + # TODO: Is this really necessary? + # Force kill any existing "workfile" instances + for instance in self.create_context.instances: + if instance.family == self.family: + self.log.debug(f"Removing instance: {instance}") + self._remove_instance_from_context(instance) + + project_name = legacy_io.Session["AVALON_PROJECT"] + asset_name = legacy_io.Session["AVALON_ASSET"] + task_name = legacy_io.Session["AVALON_TASK"] + host_name = legacy_io.Session["AVALON_APP"] + + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, task_name, asset_doc, + project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": self.default_variant + } + data.update(self.get_dynamic_data( + self.default_variant, task_name, asset_doc, + project_name, host_name + )) + + instance = CreatedInstance( + self.family, subset_name, data, self + ) + instance.transient_data["comp"] = comp + instance.transient_data["tool"] = None + self._add_instance_to_context(instance) + + self._imprint(comp, data) + + def get_icon(self): + return qtawesome.icon("fa.file-o", color="white") + + def _imprint(self, comp, data): + + # TODO: Should this keys persist or not? I'd prefer not + # Do not persist the current context for the Workfile + for key in ["variant", "subset", "asset", "task"]: + data.pop(key, None) + + # Flatten any potential nested dicts + data = flatten_dict(data, separator=".") + + # Prefix with data key openpype.workfile + data = {f"{self.data_key}.{key}" for key, value in data.items()} + comp.SetData(data) From f555c1bf9a11200d8a48a9a68bac6f5e4695e347 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:00:35 +0200 Subject: [PATCH 022/160] Shush hound --- .../hosts/fusion/plugins/publish/validate_unique_subsets.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 28bab59949..5b6ceb2fdb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -38,7 +38,8 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): asset=asset, subset=subset, instances=instances - )) + ) + ) invalid.extend(instances) # Return tools for the invalid instances so they can be selected From b8f4a0a3969b841f50cb66f15e08717d3d3cd890 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:41:55 +0200 Subject: [PATCH 023/160] Specifiy families explicitly (to avoid issues with workfile family not having a `tool`) --- openpype/hosts/fusion/plugins/publish/collect_inputs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index 8f9857b02f..e06649da99 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -97,10 +97,15 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): label = "Collect Inputs" order = pyblish.api.CollectorOrder + 0.2 hosts = ["fusion"] + families = ["render"] def process(self, instance): # Get all upstream and include itself + if not any(instance[:]): + self.log.debug("No tool found in instance, skipping..") + return + tool = instance[0] nodes = list(iter_upstream(tool)) nodes.append(tool) From 2afc8ba573ee06ed790146eb113d1a74cbe705b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:42:38 +0200 Subject: [PATCH 024/160] Fix collector with workfile present --- .../plugins/publish/collect_instances.py | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 2f3e82fded..ad264f0478 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -46,24 +46,12 @@ class CollectInstances(pyblish.api.ContextPlugin): context.data["frameEndHandle"] = int(global_end) for instance in context: - - tool = instance.data["transientData"]["tool"] - - path = tool["Clip"][comp.TIME_UNDEFINED] - filename = os.path.basename(path) - head, padding, tail = get_frame_path(filename) - ext = os.path.splitext(path)[1] - assert tail == ext, ("Tail does not match %s" % ext) - # Include start and end render frame in label subset = instance.data["subset"] label = "{subset} ({start}-{end})".format(subset=subset, start=int(start), end=int(end)) instance.data.update({ - "path": path, - "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant? "label": label, # todo: Allow custom frame range per instance "frameStart": context.data["frameStart"], @@ -71,14 +59,32 @@ class CollectInstances(pyblish.api.ContextPlugin): "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], "fps": context.data["fps"], - "families": ["render", "review"], - "family": "render", - - # Backwards compatibility: embed tool in instance.data - "tool": tool }) - # Add tool itself as member - instance.append(tool) + if instance.data["family"] == "render": + # TODO: This should probably move into a collector of + # its own for the "render" family + # This is only the case for savers currently but not + # for workfile instances. So we assume saver here. + tool = instance.data["transientData"]["tool"] + path = tool["Clip"][comp.TIME_UNDEFINED] - self.log.info("Found: \"%s\" " % path) + filename = os.path.basename(path) + head, padding, tail = get_frame_path(filename) + ext = os.path.splitext(path)[1] + assert tail == ext, ("Tail does not match %s" % ext) + + instance.data.update({ + "path": path, + "outputDir": os.path.dirname(path), + "ext": ext, # todo: should be redundant? + + "families": ["render", "review"], + "family": "render", + + # Backwards compatibility: embed tool in instance.data + "tool": tool + }) + + # Add tool itself as member + instance.append(tool) From d0ea9171b3cbd2c52ab3e027f496f75e6bb36768 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:43:25 +0200 Subject: [PATCH 025/160] Remove storage of empty tool placeholder value --- openpype/hosts/fusion/plugins/create/create_workfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 26a73abb64..19ad04f572 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -47,7 +47,6 @@ class FusionWorkfileCreator(AutoCreator): creator=self ) instance.transient_data["comp"] = comp - instance.transient_data["tool"] = None self._add_instance_to_context(instance) @@ -106,7 +105,6 @@ class FusionWorkfileCreator(AutoCreator): self.family, subset_name, data, self ) instance.transient_data["comp"] = comp - instance.transient_data["tool"] = None self._add_instance_to_context(instance) self._imprint(comp, data) From 3a952d5038b51e7d05e64d70e10054a8c9b5bd4c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 11:43:37 +0200 Subject: [PATCH 026/160] Cosmetics/readability --- openpype/hosts/fusion/plugins/create/create_workfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 19ad04f572..783d3a147a 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -102,7 +102,10 @@ class FusionWorkfileCreator(AutoCreator): )) instance = CreatedInstance( - self.family, subset_name, data, self + family=self.family, + subset_name=subset_name, + data=data, + creator=self ) instance.transient_data["comp"] = comp self._add_instance_to_context(instance) From 00d9aa216bd8689862a189eb32fcd1dd692303fe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:01:11 +0200 Subject: [PATCH 027/160] Apply to workfile family - like other hosts do --- .../fusion/plugins/publish/increment_current_file_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py b/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py index 5c595638e9..42891446f7 100644 --- a/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py +++ b/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py @@ -11,7 +11,7 @@ class FusionIncrementCurrentFile(pyblish.api.ContextPlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["fusion"] - families = ["render.farm"] + families = ["workfile"] optional = True def process(self, context): From 34c1346961a621726dfc4f9352606c038d650ec4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:01:28 +0200 Subject: [PATCH 028/160] Refactor filename --- ...crement_current_file_deadline.py => increment_current_file.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/hosts/fusion/plugins/publish/{increment_current_file_deadline.py => increment_current_file.py} (100%) diff --git a/openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py b/openpype/hosts/fusion/plugins/publish/increment_current_file.py similarity index 100% rename from openpype/hosts/fusion/plugins/publish/increment_current_file_deadline.py rename to openpype/hosts/fusion/plugins/publish/increment_current_file.py From d6080593e3213df882b5e4b6dc112223531d109e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:02:16 +0200 Subject: [PATCH 029/160] Include workfile family --- openpype/hosts/fusion/plugins/publish/save_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index 0cdfafa095..a249c453d8 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin): label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["fusion"] - families = ["render"] + families = ["render", "workfile"] def process(self, context): From eed65758e651acf372d6814b4d7061990d6ad993 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 Sep 2022 12:21:31 +0200 Subject: [PATCH 030/160] Move Submit Fusion Deadline to Deadline Module --- .../deadline/plugins/publish/submit_fusion_deadline.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{hosts/fusion/plugins/publish/submit_deadline.py => modules/deadline/plugins/publish/submit_fusion_deadline.py} (100%) diff --git a/openpype/hosts/fusion/plugins/publish/submit_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py similarity index 100% rename from openpype/hosts/fusion/plugins/publish/submit_deadline.py rename to openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py From de15f1bc3a135738f218fe27db928aef5cdb1af9 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 18:50:21 +0100 Subject: [PATCH 031/160] Updated render_local.py to not only process the first instance Moved the __hasRun to render_once() so the check only happens with the rendering. Currently only the first render node gets the representations added --- .../fusion/plugins/publish/render_local.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 53d8eb64e1..49aaf63a61 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -18,15 +18,9 @@ class Fusionlocal(pyblish.api.InstancePlugin): def process(self, instance): - # This plug-in runs only once and thus assumes all instances - # currently will render the same frame range context = instance.context - key = f"__hasRun{self.__class__.__name__}" - if context.data.get(key, False): - return - - context.data[key] = True - + + # Start render self.render_once(context) frame_start = context.data["frameStartHandle"] @@ -60,6 +54,14 @@ class Fusionlocal(pyblish.api.InstancePlugin): def render_once(self, context): """Render context comp only once, even with more render instances""" + + # This plug-in assumes all render nodes get rendered at the same time + # to speed up the rendering. The check below makes sure that we only + # execute the rendering once and not for each instance. + key = f"__hasRun{self.__class__.__name__}" + if context.data.get(key, False): + return + context.data[key] = True current_comp = context.data["currentComp"] frame_start = context.data["frameStartHandle"] From d8041562d0e08afd7dfc34158591142158d51e0e Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 18:52:50 +0100 Subject: [PATCH 032/160] Fixed Hounds notes --- openpype/hosts/fusion/plugins/publish/render_local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 49aaf63a61..c22074d6c6 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -19,7 +19,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): def process(self, instance): context = instance.context - + # Start render self.render_once(context) @@ -54,7 +54,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): def render_once(self, context): """Render context comp only once, even with more render instances""" - + # This plug-in assumes all render nodes get rendered at the same time # to speed up the rendering. The check below makes sure that we only # execute the rendering once and not for each instance. From 31d7b18e9ff6d5561ddc9d7ea2d003ae3a5c86bd Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 19:18:41 +0100 Subject: [PATCH 033/160] Changed to f-string --- openpype/hosts/fusion/plugins/create/create_saver.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 99aa7583f1..59fc198243 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -39,8 +39,9 @@ class CreateSaver(Creator): # Check file format settings are available if saver[file_format] is None: - raise RuntimeError("File format is not set to {}, " - "this is a bug".format(file_format)) + raise RuntimeError( + f"File format is not set to {file_format}, this is a bug" + ) # Set file format attributes saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other @@ -110,7 +111,7 @@ class CreateSaver(Creator): def _imprint(self, tool, data): # Save all data in a "openpype.{key}" = value data for key, value in data.items(): - tool.SetData("openpype.{}".format(key), value) + tool.SetData(f"openpype.{key}", value) def _update_tool_with_data(self, tool, data): """Update tool node name and output path based on subset data""" @@ -123,7 +124,7 @@ class CreateSaver(Creator): # Subset change detected # Update output filepath workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - filename = "{}..exr".format(subset) + filename = f"{subset}..exr" filepath = os.path.join(workdir, "render", subset, filename) tool["Clip"] = filepath From 4d3442db70c025a67e5503fb8a728e2c3acd7b96 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 19:19:24 +0100 Subject: [PATCH 034/160] Add subset to instance_data --- openpype/hosts/fusion/plugins/create/create_saver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 59fc198243..5725b5d0d1 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -33,6 +33,7 @@ class CreateSaver(Creator): args = (-32768, -32768) # Magical position numbers saver = comp.AddTool("Saver", *args) + instance_data["subset"] = subset_name self._update_tool_with_data(saver, data=instance_data) saver["OutputFormat"] = file_format From 8afd6f4f359cf1bdea8f08386a983a1782e35871 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 19:19:52 +0100 Subject: [PATCH 035/160] Change render depth to Auto instead of forcing int16 as it was --- openpype/hosts/fusion/plugins/create/create_saver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 5725b5d0d1..439064770e 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -45,8 +45,9 @@ class CreateSaver(Creator): ) # Set file format attributes - saver[file_format]["Depth"] = 1 # int8 | int16 | float32 | other - saver[file_format]["SaveAlpha"] = 0 + saver[file_format]["Depth"] = 0 # Auto | float16 | float32 + # TODO Is this needed? + saver[file_format]["SaveAlpha"] = 1 self._imprint(saver, instance_data) From f840347f4e27a3f7474fe7e29facf9fa02f4e7ef Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 19:20:20 +0100 Subject: [PATCH 036/160] Import get_current_comp from API directly --- openpype/hosts/fusion/plugins/create/create_workfile.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 783d3a147a..05dbcddd52 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -2,7 +2,9 @@ import collections import qtawesome -import openpype.hosts.fusion.api as api +from openpype.hosts.fusion.api import ( + get_current_comp +) from openpype.client import get_asset_by_name from openpype.pipeline import ( AutoCreator, @@ -35,7 +37,7 @@ class FusionWorkfileCreator(AutoCreator): def collect_instances(self): - comp = api.get_current_comp() + comp = get_current_comp() data = comp.GetData(self.data_key) if not data: return @@ -69,7 +71,7 @@ class FusionWorkfileCreator(AutoCreator): def create(self, options=None): - comp = api.get_current_comp() + comp = get_current_comp() if not comp: self.log.error("Unable to find current comp") return From f5a40056ab3e8cf18f5562d8961174fe422ce3e1 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 19:21:00 +0100 Subject: [PATCH 037/160] Pass "data" to get_dynamic_data() was missing --- openpype/hosts/fusion/plugins/create/create_workfile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 05dbcddd52..917780c56e 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -99,8 +99,12 @@ class FusionWorkfileCreator(AutoCreator): "variant": self.default_variant } data.update(self.get_dynamic_data( - self.default_variant, task_name, asset_doc, - project_name, host_name + self.default_variant, + task_name, + asset_doc, + project_name, + host_name, + data )) instance = CreatedInstance( From f0cd353301c6192ec0203a83de2ec7b31e4979c6 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 23 Feb 2023 19:29:03 +0100 Subject: [PATCH 038/160] Fixed Hound's notes --- openpype/hosts/fusion/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index eb097e5c5b..2d0a1da8fa 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -463,4 +463,4 @@ class FusionEventHandler(QtCore.QObject): # Comp Opened elif what in {"Comp_Opened"}: - emit_event("open", data=event) \ No newline at end of file + emit_event("open", data=event) From 8a6efcd6ef9699f977e58ab8fc6042a6e8e3a0f5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Feb 2023 10:00:25 +0100 Subject: [PATCH 039/160] Nuke: new version builder from tempate wip --- .../nuke/api/workfile_template_builder.py | 3 +- .../workfile/workfile_template_builder.py | 29 ++++++++++++++++++- .../defaults/project_settings/nuke.json | 12 +++++++- .../schema_templated_workfile_build.json | 6 ++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 1b81f24e86..739c10d56b 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -1,3 +1,4 @@ +import os import collections import nuke @@ -45,7 +46,7 @@ class NukeTemplateBuilder(AbstractTemplateBuilder): get_template_preset implementation) Returns: - bool: Wether the template was succesfully imported or not + bool: Wether the template was successfully imported or not """ # TODO check if the template is already imported diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 119e4aaeb7..6bbe5f5d13 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -28,6 +28,7 @@ from openpype.settings import ( get_project_settings, get_system_settings, ) +from openpype.host import IWorkfileHost from openpype.host import HostBase from openpype.lib import ( Logger, @@ -416,7 +417,8 @@ class AbstractTemplateBuilder(object): self, template_path=None, level_limit=None, - keep_placeholders=None + keep_placeholders=None, + create_first_version=None ): """Main callback for building workfile from template path. @@ -433,6 +435,7 @@ class AbstractTemplateBuilder(object): keep_placeholders (bool): Add flag to placeholder data for hosts to decide if they want to remove placeholder after it is used. + create_first_version (bool): create first version of a workfile """ template_preset = self.get_template_preset() @@ -441,6 +444,11 @@ class AbstractTemplateBuilder(object): if keep_placeholders is None: keep_placeholders = template_preset["keep_placeholder"] + if create_first_version is None: + create_first_version = template_preset["create_first_version"] + + if create_first_version: + self.create_first_workfile_version() self.import_template(template_path) self.populate_scene_placeholders( @@ -492,6 +500,25 @@ class AbstractTemplateBuilder(object): pass + @abstractmethod + def create_first_workfile_version(self): + """ + Create first version of workfile. + + Should load the content of template into scene so + 'populate_scene_placeholders' can be started. + + Args: + template_path (str): Fullpath for current task and + host's template file. + """ + last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") + # Save current scene, continue to open file + if isinstance(self.host, IWorkfileHost): + self.host.save_workfile(last_workfile_path) + else: + self.host.save_file(last_workfile_path) + def _prepare_placeholders(self, placeholders): """Run preparation part for placeholders on plugins. diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 2545411e0a..c249955dc8 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -565,7 +565,17 @@ ] }, "templated_workfile_build": { - "profiles": [] + "profiles": [ + { + "task_types": [ + "Compositing" + ], + "task_names": [], + "path": "{project[name]}/templates/comp.nk", + "keep_placeholder": true, + "create_first_version": true + } + ] }, "filters": {} } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index b244460bbf..7bab28fd88 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -34,6 +34,12 @@ "label": "Keep placeholders", "type": "boolean", "default": true + }, + { + "key": "create_first_version", + "label": "Create first version", + "type": "boolean", + "default": true } ] } From 2ed1a97864ba7c9b557b6159c03a78056d804b6f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Feb 2023 14:22:38 +0100 Subject: [PATCH 040/160] nuke: start new workfile from tempate wip --- openpype/hosts/nuke/api/lib.py | 12 ++++++++++- openpype/hosts/nuke/api/pipeline.py | 3 ++- .../nuke/api/workfile_template_builder.py | 20 +++++++++++++++++- .../workfile/workfile_template_builder.py | 21 +++++++------------ 4 files changed, 39 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index c08db978d3..2b7aaa9d70 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -48,7 +48,6 @@ from openpype.pipeline.colorspace import ( get_imageio_config ) from openpype.pipeline.workfile import BuildWorkfile - from . import gizmo_menu from .constants import ASSIST @@ -2678,6 +2677,17 @@ def process_workfile_builder(): open_file(last_workfile_path) +def start_workfile_template_builder(): + from .workfile_template_builder import ( + build_workfile_template + ) + + # to avoid looping of the callback, remove it! + # nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") + log.info("Starting workfile template builder...") + build_workfile_template() + + @deprecated def recreate_instance(origin_node, avalon_data=None): """Recreate input instance to different data diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 2496d66c1d..30270a4e5f 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -33,6 +33,7 @@ from .lib import ( add_publish_knob, WorkfileSettings, process_workfile_builder, + start_workfile_template_builder, launch_workfiles_app, check_inventory_versions, set_avalon_knob_data, @@ -48,7 +49,6 @@ from .workfile_template_builder import ( NukePlaceholderLoadPlugin, NukePlaceholderCreatePlugin, build_workfile_template, - update_workfile_template, create_placeholder, update_placeholder, ) @@ -155,6 +155,7 @@ def add_nuke_callbacks(): # Set context settings. nuke.addOnCreate( workfile_settings.set_context_settings, nodeClass="Root") + nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") nuke.addOnCreate(process_workfile_builder, nodeClass="Root") diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 739c10d56b..1c0a41456a 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -15,7 +15,7 @@ from openpype.pipeline.workfile.workfile_template_builder import ( from openpype.tools.workfile_template_build import ( WorkfileBuildPlaceholderDialog, ) - +from openpype.host import IWorkfileHost from .lib import ( find_free_space_to_paste_nodes, get_extreme_positions, @@ -56,6 +56,24 @@ class NukeTemplateBuilder(AbstractTemplateBuilder): return True + def create_first_workfile_version(self): + """ + Create first version of workfile. + + Should load the content of template into scene so + 'populate_scene_placeholders' can be started. + + Args: + template_path (str): Fullpath for current task and + host's template file. + """ + last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") + # Save current scene, continue to open file + if isinstance(self.host, IWorkfileHost): + self.host.save_workfile(last_workfile_path) + else: + self.host.save_file(last_workfile_path) + class NukePlaceholderPlugin(PlaceholderPlugin): node_color = 4278190335 diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 6bbe5f5d13..1758c30a8b 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -505,19 +505,8 @@ class AbstractTemplateBuilder(object): """ Create first version of workfile. - Should load the content of template into scene so - 'populate_scene_placeholders' can be started. - - Args: - template_path (str): Fullpath for current task and - host's template file. """ - last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") - # Save current scene, continue to open file - if isinstance(self.host, IWorkfileHost): - self.host.save_workfile(last_workfile_path) - else: - self.host.save_file(last_workfile_path) + pass def _prepare_placeholders(self, placeholders): """Run preparation part for placeholders on plugins. @@ -702,6 +691,8 @@ class AbstractTemplateBuilder(object): # switch to remove placeholders after they are used keep_placeholder = profile.get("keep_placeholder") + create_first_version = profile.get("create_first_version") + # backward compatibility, since default is True if keep_placeholder is None: keep_placeholder = True @@ -735,7 +726,8 @@ class AbstractTemplateBuilder(object): self.log.info("Found template at: '{}'".format(path)) return { "path": path, - "keep_placeholder": keep_placeholder + "keep_placeholder": keep_placeholder, + "create_first_version": create_first_version } solved_path = None @@ -764,7 +756,8 @@ class AbstractTemplateBuilder(object): return { "path": solved_path, - "keep_placeholder": keep_placeholder + "keep_placeholder": keep_placeholder, + "create_first_version": create_first_version } From 1ef786afb8c7dacfd219bdf3733a52e777082c3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Feb 2023 14:39:12 +0100 Subject: [PATCH 041/160] added option to use new creating system in workfile template builder --- .../maya/api/workfile_template_builder.py | 2 + .../workfile/workfile_template_builder.py | 97 ++++++++++++++----- 2 files changed, 74 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 2f550e787a..90ab6e21e0 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -22,6 +22,8 @@ PLACEHOLDER_SET = "PLACEHOLDERS_SET" class MayaTemplateBuilder(AbstractTemplateBuilder): """Concrete implementation of AbstractTemplateBuilder for maya""" + use_legacy_creators = True + def import_template(self, path): """Import template into current scene. Block if a template is already loaded. diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 119e4aaeb7..0167224cb0 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,7 +43,8 @@ from openpype.pipeline.load import ( load_with_repre_context, ) from openpype.pipeline.create import ( - discover_legacy_creator_plugins + discover_legacy_creator_plugins, + CreateContext, ) @@ -91,6 +92,7 @@ class AbstractTemplateBuilder(object): """ _log = None + use_legacy_creators = False def __init__(self, host): # Get host name @@ -110,6 +112,7 @@ class AbstractTemplateBuilder(object): self._placeholder_plugins = None self._loaders_by_name = None self._creators_by_name = None + self._create_context = None self._system_settings = None self._project_settings = None @@ -171,6 +174,14 @@ class AbstractTemplateBuilder(object): .get("type") ) + @property + def create_context(self): + if self._create_context is None: + self._create_context = CreateContext( + self.host, discover_publish_plugins=False + ) + return self._create_context + def get_placeholder_plugin_classes(self): """Get placeholder plugin classes that can be used to build template. @@ -235,18 +246,29 @@ class AbstractTemplateBuilder(object): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name + def _collect_legacy_creators(self): + creators_by_name = {} + for creator in discover_legacy_creator_plugins(): + if not creator.enabled: + continue + creator_name = creator.__name__ + if creator_name in creators_by_name: + raise KeyError( + "Duplicated creator name {} !".format(creator_name) + ) + creators_by_name[creator_name] = creator + self._creators_by_name = creators_by_name + + def _collect_creators(self): + self._creators_by_name = dict(self.create_context.creators) + def get_creators_by_name(self): if self._creators_by_name is None: - self._creators_by_name = {} - for creator in discover_legacy_creator_plugins(): - if not creator.enabled: - continue - creator_name = creator.__name__ - if creator_name in self._creators_by_name: - raise KeyError( - "Duplicated creator name {} !".format(creator_name) - ) - self._creators_by_name[creator_name] = creator + if self.use_legacy_creators: + self._collect_legacy_creators() + else: + self._collect_creators() + return self._creators_by_name def get_shared_data(self, key): @@ -1579,6 +1601,8 @@ class PlaceholderCreateMixin(object): placeholder (PlaceholderItem): Placeholder item with information about requested publishable instance. """ + + legacy_create = self.builder.use_legacy_creators creator_name = placeholder.data["creator"] create_variant = placeholder.data["create_variant"] @@ -1589,17 +1613,28 @@ class PlaceholderCreateMixin(object): task_name = legacy_io.Session["AVALON_TASK"] asset_name = legacy_io.Session["AVALON_ASSET"] - # get asset id - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) - assert asset_doc, "No current asset found in Session" - asset_id = asset_doc['_id'] + if legacy_create: + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["_id"] + ) + assert asset_doc, "No current asset found in Session" + subset_name = creator_plugin.get_subset_name( + create_variant, + task_name, + asset_doc["_id"], + project_name + ) - subset_name = creator_plugin.get_subset_name( - create_variant, - task_name, - asset_id, - project_name - ) + else: + asset_doc = get_asset_by_name(project_name, asset_name) + assert asset_doc, "No current asset found in Session" + subset_name = creator_plugin.get_subset_name( + create_variant, + task_name, + asset_doc, + project_name, + self.builder.host_name + ) creator_data = { "creator_name": creator_name, @@ -1612,10 +1647,22 @@ class PlaceholderCreateMixin(object): # compile subset name from variant try: - creator_instance = creator_plugin( - subset_name, - asset_name - ).process() + if legacy_create: + creator_instance = creator_plugin( + subset_name, + asset_name + ).process() + else: + creator_instance = creator_plugin.create( + subset_name, + { + "asset": asset_doc["name"], + "task": task_name, + "family": creator_plugin.family, + "variant": create_variant + }, + {} + ) except Exception: failed = True From ee3e346c8df68515d72221bb2e3fe84ab92e9b0e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Feb 2023 14:53:49 +0100 Subject: [PATCH 042/160] Global: refactory colormanaged exctractor into plugin mixin --- .../plugins/publish/extract_render_local.py | 3 +- openpype/pipeline/publish/__init__.py | 4 +- openpype/pipeline/publish/publish_plugins.py | 40 ++++++++++--------- .../publish/extract_colorspace_data.py | 3 +- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index b99a7a9548..4d7ade9c7a 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -9,7 +9,8 @@ from openpype.pipeline import publish from openpype.lib import collect_frames -class NukeRenderLocal(publish.ExtractorColormanaged): +class NukeRenderLocal(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): """Render the current Nuke composition locally. Extract the result of savers by starting a comp render diff --git a/openpype/pipeline/publish/__init__.py b/openpype/pipeline/publish/__init__.py index 05ba1c9c33..36252c9f3d 100644 --- a/openpype/pipeline/publish/__init__.py +++ b/openpype/pipeline/publish/__init__.py @@ -19,7 +19,7 @@ from .publish_plugins import ( RepairContextAction, Extractor, - ExtractorColormanaged, + ColormanagedPyblishPluginMixin ) from .lib import ( @@ -64,7 +64,7 @@ __all__ = ( "RepairContextAction", "Extractor", - "ExtractorColormanaged", + "ColormanagedPyblishPluginMixin", "get_publish_template_name", diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index e2ae893aa9..0142919e76 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -3,7 +3,7 @@ from abc import ABCMeta from pprint import pformat import pyblish.api from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin - +from openpype.lib.transcoding import VIDEO_EXTENSIONS, IMAGE_EXTENSIONS from openpype.lib import BoolDef from .lib import ( @@ -288,24 +288,25 @@ class Extractor(pyblish.api.InstancePlugin): return get_instance_staging_dir(instance) -class ExtractorColormanaged(Extractor): - """Extractor base for color managed image data. - - Each Extractor intended to export pixel data representation - should inherit from this class to allow color managed data. - Class implements "get_colorspace_settings" and - "set_representation_colorspace" functions used - for injecting colorspace data to representation data for farther - integration into db document. +class ColormanagedPyblishPluginMixin(object): + """Mixin for colormanaged plugins. + This class is used to set colorspace data to a publishing + representation. It contains a static method, + get_colorspace_settings, which returns config and + file rules data for the host context. + It also contains a method, set_representation_colorspace, + which sets colorspace data to the representation. + The allowed file extensions are listed in the allowed_ext variable. + he method first checks if the file extension is in + the list of allowed extensions. If it is, it then gets the + colorspace settings from the host context and gets a + matching colorspace from rules. Finally, it infuses this + data into the representation. """ - - allowed_ext = [ - "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", - "mp4", "m4v", "mxf", "iff", "z", "ifl", "jpeg", "jpg", "jfif", "lut", - "1dl", "exr", "pic", "png", "ppm", "pnm", "pgm", "pbm", "rla", "rpf", - "sgi", "rgba", "rgb", "bw", "tga", "tiff", "tif", "img" - ] + allowed_ext = set( + ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) + ) @staticmethod def get_colorspace_settings(context): @@ -375,7 +376,10 @@ class ExtractorColormanaged(Extractor): ext = representation["ext"] # check extension self.log.debug("__ ext: `{}`".format(ext)) - if ext.lower() not in self.allowed_ext: + + # check if ext in lower case is in self.allowed_ext + if ext.lstrip(".").lower() not in self.allowed_ext: + self.log.debug("Extension is not in allowed extensions.") return if colorspace_settings is None: diff --git a/openpype/plugins/publish/extract_colorspace_data.py b/openpype/plugins/publish/extract_colorspace_data.py index 611fb91cbb..363df28fb5 100644 --- a/openpype/plugins/publish/extract_colorspace_data.py +++ b/openpype/plugins/publish/extract_colorspace_data.py @@ -2,7 +2,8 @@ import pyblish.api from openpype.pipeline import publish -class ExtractColorspaceData(publish.ExtractorColormanaged): +class ExtractColorspaceData(publish.Extractor, + publish.ColormanagedPyblishPluginMixin): """ Inject Colorspace data to available representations. Input data: From 76f312a3ff026d04feda901b98bc0ad523e3b00b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Feb 2023 15:04:54 +0100 Subject: [PATCH 043/160] Nuke: adding colorspace to representation when rendered mode --- .../hosts/nuke/plugins/publish/collect_writes.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 3054e5a30c..2b741426e6 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -3,9 +3,10 @@ from pprint import pformat import nuke import pyblish.api from openpype.hosts.nuke import api as napi +from openpype.pipeline import publish - -class CollectNukeWrites(pyblish.api.InstancePlugin): +class CollectNukeWrites(pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin): """Collect all write nodes.""" order = pyblish.api.CollectorOrder - 0.48 @@ -128,6 +129,12 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): else: representation['files'] = collected_frames + # inject colorspace data + self.set_representation_colorspace( + representation, instance.context, + colorspace=colorspace + ) + instance.data["representations"].append(representation) self.log.info("Publishing rendered frames ...") @@ -147,6 +154,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin): # get colorspace and add to version data colorspace = napi.get_colorspace_from_node(write_node) + + # TODO: remove this when we have proper colorspace support version_data = { "colorspace": colorspace } From 9bb36864be3911b25c37c873d68ae4871fdcf57a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Feb 2023 15:05:16 +0100 Subject: [PATCH 044/160] Nuke: colorspace from node unified --- openpype/hosts/nuke/plugins/publish/extract_render_local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_render_local.py b/openpype/hosts/nuke/plugins/publish/extract_render_local.py index 4d7ade9c7a..e5feda4cd8 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_render_local.py +++ b/openpype/hosts/nuke/plugins/publish/extract_render_local.py @@ -4,7 +4,7 @@ import shutil import pyblish.api import clique import nuke - +from openpype.hosts.nuke import api as napi from openpype.pipeline import publish from openpype.lib import collect_frames @@ -86,7 +86,7 @@ class NukeRenderLocal(publish.Extractor, ) ext = node["file_type"].value() - colorspace = node["colorspace"].value() + colorspace = napi.get_colorspace_from_node(node) if "representations" not in instance.data: instance.data["representations"] = [] From b7e99dacb8b5107a90cbdd933c6b5985dc5bbb79 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Feb 2023 15:45:12 +0100 Subject: [PATCH 045/160] fix spaces --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 0167224cb0..7ef2e7378b 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1653,7 +1653,7 @@ class PlaceholderCreateMixin(object): asset_name ).process() else: - creator_instance = creator_plugin.create( + creator_instance = creator_plugin.create( subset_name, { "asset": asset_doc["name"], From 6b2c10da04cb4494d709d612ba06d9d5b7482bf4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Feb 2023 16:51:18 +0100 Subject: [PATCH 046/160] use 'create' method on create context to trigger creation --- .../pipeline/workfile/workfile_template_builder.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 7ef2e7378b..26735d77d0 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1653,15 +1653,11 @@ class PlaceholderCreateMixin(object): asset_name ).process() else: - creator_instance = creator_plugin.create( - subset_name, - { - "asset": asset_doc["name"], - "task": task_name, - "family": creator_plugin.family, - "variant": create_variant - }, - {} + creator_instance = self.create_context.create( + creator_plugin.identifier, + create_variant, + asset_doc, + task_name=task_name ) except Exception: From 9acf634d1363d08a580bc8341f45419f2effe721 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 28 Feb 2023 17:09:43 +0100 Subject: [PATCH 047/160] fix attribute access --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 26735d77d0..6ffe0116e5 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1653,7 +1653,7 @@ class PlaceholderCreateMixin(object): asset_name ).process() else: - creator_instance = self.create_context.create( + creator_instance = self.builder.create_context.create( creator_plugin.identifier, create_variant, asset_doc, From 7c90b6616d50d8106dd6be0811af372d0e5b486c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 28 Feb 2023 17:32:49 +0100 Subject: [PATCH 048/160] adding headless to creators and workfile builder abstraction --- openpype/hosts/nuke/api/plugin.py | 6 +++++- openpype/hosts/nuke/plugins/create/create_write_image.py | 2 +- .../hosts/nuke/plugins/create/create_write_prerender.py | 2 +- openpype/hosts/nuke/plugins/create/create_write_render.py | 2 +- openpype/pipeline/workfile/workfile_template_builder.py | 6 ++++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 160ca820a4..9518598238 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -239,7 +239,11 @@ class NukeCreator(NewCreator): def get_pre_create_attr_defs(self): return [ - BoolDef("use_selection", label="Use selection") + BoolDef( + "use_selection", + default=not self.create_context.headless, + label="Use selection" + ) ] def get_creator_settings(self, project_settings, settings_key=None): diff --git a/openpype/hosts/nuke/plugins/create/create_write_image.py b/openpype/hosts/nuke/plugins/create/create_write_image.py index 1e23b3ad7f..d38253ab2f 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_image.py +++ b/openpype/hosts/nuke/plugins/create/create_write_image.py @@ -35,7 +35,7 @@ class CreateWriteImage(napi.NukeWriteCreator): attr_defs = [ BoolDef( "use_selection", - default=True, + default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum(), diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index a15f362dd1..d0d7f8edfd 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -37,7 +37,7 @@ class CreateWritePrerender(napi.NukeWriteCreator): attr_defs = [ BoolDef( "use_selection", - default=True, + default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum() diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 481d1d2201..4e0b42361d 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -34,7 +34,7 @@ class CreateWriteRender(napi.NukeWriteCreator): attr_defs = [ BoolDef( "use_selection", - default=True, + default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum() diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 6ffe0116e5..6a99314f48 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -178,7 +178,9 @@ class AbstractTemplateBuilder(object): def create_context(self): if self._create_context is None: self._create_context = CreateContext( - self.host, discover_publish_plugins=False + self.host, + discover_publish_plugins=False, + headless=True ) return self._create_context @@ -1660,7 +1662,7 @@ class PlaceholderCreateMixin(object): task_name=task_name ) - except Exception: + except: failed = True self.create_failed(placeholder, creator_data) From 491eb3e75010d2f5214385145d05120ce736f93f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Mar 2023 17:27:16 +0100 Subject: [PATCH 049/160] create first workfile version function to global abstraction --- openpype/hosts/nuke/api/lib.py | 2 +- .../nuke/api/workfile_template_builder.py | 22 +---------- .../workfile/workfile_template_builder.py | 39 ++++++++++++++++--- 3 files changed, 37 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 2b7aaa9d70..cd31e42690 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2685,7 +2685,7 @@ def start_workfile_template_builder(): # to avoid looping of the callback, remove it! # nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") log.info("Starting workfile template builder...") - build_workfile_template() + build_workfile_template(run_from_callback=True) @deprecated diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 1c0a41456a..80db0d160c 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -56,24 +56,6 @@ class NukeTemplateBuilder(AbstractTemplateBuilder): return True - def create_first_workfile_version(self): - """ - Create first version of workfile. - - Should load the content of template into scene so - 'populate_scene_placeholders' can be started. - - Args: - template_path (str): Fullpath for current task and - host's template file. - """ - last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") - # Save current scene, continue to open file - if isinstance(self.host, IWorkfileHost): - self.host.save_workfile(last_workfile_path) - else: - self.host.save_file(last_workfile_path) - class NukePlaceholderPlugin(PlaceholderPlugin): node_color = 4278190335 @@ -966,9 +948,9 @@ class NukePlaceholderCreatePlugin( siblings_input.setInput(0, copy_output) -def build_workfile_template(*args): +def build_workfile_template(*args, **kwargs): builder = NukeTemplateBuilder(registered_host()) - builder.build_template() + builder.build_template(*args, **kwargs) def update_workfile_template(*args): diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 3dd02ea14d..d73168194e 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -442,7 +442,8 @@ class AbstractTemplateBuilder(object): template_path=None, level_limit=None, keep_placeholders=None, - create_first_version=None + create_first_version=None, + run_from_callback=False ): """Main callback for building workfile from template path. @@ -460,6 +461,9 @@ class AbstractTemplateBuilder(object): hosts to decide if they want to remove placeholder after it is used. create_first_version (bool): create first version of a workfile + run_from_callback (bool): If True, it might create first version + but ignore process if version is created + """ template_preset = self.get_template_preset() @@ -471,8 +475,14 @@ class AbstractTemplateBuilder(object): if create_first_version is None: create_first_version = template_preset["create_first_version"] - if create_first_version: - self.create_first_workfile_version() + # run creation of first version only if it is + # run from callback and no new version is created + first_creation = False + if create_first_version and run_from_callback: + first_creation = not self.create_first_workfile_version() + + if first_creation: + return self.import_template(template_path) self.populate_scene_placeholders( @@ -524,13 +534,32 @@ class AbstractTemplateBuilder(object): pass - @abstractmethod def create_first_workfile_version(self): """ Create first version of workfile. + Should load the content of template into scene so + 'populate_scene_placeholders' can be started. + + Args: + template_path (str): Fullpath for current task and + host's template file. """ - pass + last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") + if os.path.exists(last_workfile_path): + # ignore in case workfile existence + self.log.info("Workfile already exists, skipping creation.") + return False + + # Save current scene, continue to open file + if isinstance(self.host, IWorkfileHost): + self.host.save_workfile(last_workfile_path) + else: + self.host.save_file(last_workfile_path) + + # Confirm creation of first version + return True + def _prepare_placeholders(self, placeholders): """Run preparation part for placeholders on plugins. From e1fa9f7c3133c359475dd15145b405406aada8f5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Mar 2023 17:30:31 +0100 Subject: [PATCH 050/160] adding noqa for hound --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 6a99314f48..2d768d216f 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1662,7 +1662,7 @@ class PlaceholderCreateMixin(object): task_name=task_name ) - except: + except: # noqa: E722 failed = True self.create_failed(placeholder, creator_data) From 2e83019efa4d2831e5299f33bb4052e263341949 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 1 Mar 2023 17:31:45 +0100 Subject: [PATCH 051/160] hound --- openpype/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 2d768d216f..27214af79f 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1662,7 +1662,7 @@ class PlaceholderCreateMixin(object): task_name=task_name ) - except: # noqa: E722 + except: # noqa: E722 failed = True self.create_failed(placeholder, creator_data) From 5bb204cacbfd0f9769f2f4112e50f6e65b4a7f6e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 2 Mar 2023 11:30:42 +0100 Subject: [PATCH 052/160] nuke flip order --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 2b741426e6..f6acd24f99 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -67,6 +67,9 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, write_file_path = nuke.filename(write_node) output_dir = os.path.dirname(write_file_path) + # get colorspace and add to version data + colorspace = napi.get_colorspace_from_node(write_node) + self.log.debug('output dir: {}'.format(output_dir)) if render_target == "frames": @@ -152,9 +155,6 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, instance.data["farm"] = True self.log.info("Farm rendering ON ...") - # get colorspace and add to version data - colorspace = napi.get_colorspace_from_node(write_node) - # TODO: remove this when we have proper colorspace support version_data = { "colorspace": colorspace From f0997710818d3ca2f5ece87aed242ddf4c139a6c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 2 Mar 2023 11:36:17 +0100 Subject: [PATCH 053/160] hound --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index f6acd24f99..858fa79a4b 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -5,6 +5,7 @@ import pyblish.api from openpype.hosts.nuke import api as napi from openpype.pipeline import publish + class CollectNukeWrites(pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin): """Collect all write nodes.""" From 59cea8c7d070bfe1b757d5f12be96c8f4d1e9a47 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 12:15:37 +0100 Subject: [PATCH 054/160] Catch for each instance whether the render succeeded or not --- .../fusion/plugins/publish/render_local.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index c22074d6c6..27bd312048 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -59,24 +59,27 @@ class Fusionlocal(pyblish.api.InstancePlugin): # to speed up the rendering. The check below makes sure that we only # execute the rendering once and not for each instance. key = f"__hasRun{self.__class__.__name__}" - if context.data.get(key, False): - return - context.data[key] = True + if key not in context.data: + # We initialize as false to indicate it wasn't successful yet + # so we can keep track of whether Fusion succeeded + context.data[key] = False - current_comp = context.data["currentComp"] - frame_start = context.data["frameStartHandle"] - frame_end = context.data["frameEndHandle"] + current_comp = context.data["currentComp"] + frame_start = context.data["frameStartHandle"] + frame_end = context.data["frameEndHandle"] + + self.log.info("Starting Fusion render") + self.log.info(f"Start frame: {frame_start}") + self.log.info(f"End frame: {frame_end}") + + with comp_lock_and_undo_chunk(current_comp): + result = current_comp.Render({ + "Start": frame_start, + "End": frame_end, + "Wait": True + }) + + context.data[key] = bool(result) - self.log.info("Starting render") - self.log.info(f"Start frame: {frame_start}") - self.log.info(f"End frame: {frame_end}") - - with comp_lock_and_undo_chunk(current_comp): - result = current_comp.Render({ - "Start": frame_start, - "End": frame_end, - "Wait": True - }) - - if not result: + if context.data[key] is False: raise RuntimeError("Comp render failed") From 173ec225eba1262de024e30917673d4650feb1ee Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 Mar 2023 10:32:05 +0100 Subject: [PATCH 055/160] Set image format --- openpype/hosts/maya/api/lib_rendersettings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index f19deb0351..7d44d4eaa6 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -336,7 +336,8 @@ class RenderSettings(object): ) # Set render file format to exr - cmds.setAttr("{}.imageFormatStr".format(node), "exr", type="string") + ext = vray_render_presets["image_format"] + cmds.setAttr("{}.imageFormatStr".format(node), ext, type="string") # animType cmds.setAttr("{}.animType".format(node), 1) From 8fcf6de8c802d78effd5e5e696aa36707f84b473 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Mar 2023 12:58:00 +0100 Subject: [PATCH 056/160] Fixed hound's comments --- .../fusion/plugins/publish/render_local.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 27bd312048..3eb4cbd868 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -17,7 +17,6 @@ class Fusionlocal(pyblish.api.InstancePlugin): families = ["render.local"] def process(self, instance): - context = instance.context # Start render @@ -35,10 +34,10 @@ class Fusionlocal(pyblish.api.InstancePlugin): for frame in range(frame_start, frame_end + 1) ] repre = { - 'name': ext[1:], - 'ext': ext[1:], - 'frameStart': f"%0{len(str(frame_end))}d" % frame_start, - 'files': files, + "name": ext[1:], + "ext": ext[1:], + "frameStart": f"%0{len(str(frame_end))}d" % frame_start, + "files": files, "stagingDir": output_dir, } @@ -67,18 +66,20 @@ class Fusionlocal(pyblish.api.InstancePlugin): current_comp = context.data["currentComp"] frame_start = context.data["frameStartHandle"] frame_end = context.data["frameEndHandle"] - + self.log.info("Starting Fusion render") self.log.info(f"Start frame: {frame_start}") self.log.info(f"End frame: {frame_end}") - + with comp_lock_and_undo_chunk(current_comp): - result = current_comp.Render({ - "Start": frame_start, - "End": frame_end, - "Wait": True - }) - + result = current_comp.Render( + { + "Start": frame_start, + "End": frame_end, + "Wait": True, + } + ) + context.data[key] = bool(result) if context.data[key] is False: From f3baace6682bebfbbbae273b54c4aaf38477f4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 3 Mar 2023 13:17:46 +0100 Subject: [PATCH 057/160] Update openpype/pipeline/publish/publish_plugins.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fabià Serra Arrizabalaga --- openpype/pipeline/publish/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 0142919e76..7da61fec5e 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -298,7 +298,7 @@ class ColormanagedPyblishPluginMixin(object): It also contains a method, set_representation_colorspace, which sets colorspace data to the representation. The allowed file extensions are listed in the allowed_ext variable. - he method first checks if the file extension is in + The method first checks if the file extension is in the list of allowed extensions. If it is, it then gets the colorspace settings from the host context and gets a matching colorspace from rules. Finally, it infuses this From 0c517a12a618076fdd3fc043fb1ce80d7e1f3327 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Mar 2023 14:07:20 +0100 Subject: [PATCH 058/160] Nuke: fix the order of plugin to be after anatomy data collector also convert anatomy data with deepcopy --- openpype/hosts/nuke/plugins/publish/collect_writes.py | 2 +- openpype/pipeline/publish/publish_plugins.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_writes.py b/openpype/hosts/nuke/plugins/publish/collect_writes.py index 858fa79a4b..304b3d8f32 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/collect_writes.py @@ -10,7 +10,7 @@ class CollectNukeWrites(pyblish.api.InstancePlugin, publish.ColormanagedPyblishPluginMixin): """Collect all write nodes.""" - order = pyblish.api.CollectorOrder - 0.48 + order = pyblish.api.CollectorOrder + 0.0021 label = "Collect Writes" hosts = ["nuke", "nukeassist"] families = ["render", "prerender", "image"] diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 7da61fec5e..2df98221ba 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,3 +1,4 @@ +from copy import deepcopy import inspect from abc import ABCMeta from pprint import pformat @@ -323,7 +324,7 @@ class ColormanagedPyblishPluginMixin(object): project_name = context.data["projectName"] host_name = context.data["hostName"] - anatomy_data = context.data["anatomyData"] + anatomy_data = deepcopy(context.data["anatomyData"]) project_settings_ = context.data["project_settings"] config_data = get_imageio_config( From efac55ba8961d75e3fa1fb9d2f2860790f1e58e8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Mar 2023 14:08:00 +0100 Subject: [PATCH 059/160] Added render log per instance --- openpype/hosts/fusion/plugins/publish/render_local.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 3eb4cbd868..86c283952c 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -22,6 +22,15 @@ class Fusionlocal(pyblish.api.InstancePlugin): # Start render self.render_once(context) + # Log render status + self.log.info( + "Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format( + nm=instance.data.name, + ast=instance.data.asset, + tsk=instance.data.task, + ) + ) + frame_start = context.data["frameStartHandle"] frame_end = context.data["frameEndHandle"] path = instance.data["path"] From a63872b54b4e4aaf42e4875a2b437642b345eb27 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Mar 2023 16:07:29 +0100 Subject: [PATCH 060/160] Fixed dict data access --- openpype/hosts/fusion/plugins/publish/render_local.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 86c283952c..0eca7f6cdd 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -25,9 +25,9 @@ class Fusionlocal(pyblish.api.InstancePlugin): # Log render status self.log.info( "Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format( - nm=instance.data.name, - ast=instance.data.asset, - tsk=instance.data.task, + nm=instance.data["name"], + ast=instance.data["asset"], + tsk=instance.data["task"], ) ) From 6f0cd7fc2f04f8ff13e7a756f932ab7068c29629 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 31 Jan 2023 17:30:27 +0100 Subject: [PATCH 061/160] add visualParent None to Shots and Assets Without this, tray-publisher throws an error and stops working. --- openpype/modules/kitsu/utils/update_op_with_zou.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 2d14b38bc4..5af3a61e81 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -389,6 +389,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): "data": { "root_of": r, "tasks": {}, + "visualParent": None, }, } for r in ["Assets", "Shots"] From 76273b7376c5b65423beb3a195c4a6376f0453be Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Feb 2023 13:38:22 +0100 Subject: [PATCH 062/160] lowercase URL path to match in Kitsu Without this fix the menu-dropdown wouldn't show the correct current page in Kitsu --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index c95079e042..4793d60fc3 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -100,7 +100,7 @@ class ShowInKitsu(LauncherAction): kitsu_url = kitsu_url[:-len("/api")] sub_url = f"/productions/{project_id}" - asset_type_url = "Shots" if asset_type in shots_url else "Assets" + asset_type_url = "shots" if asset_type in shots_url else "assets" if task_id: # Go to task page From 9795e7c9105777f0ae0c4d20376d5204b23b1b00 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Feb 2023 15:52:56 +0100 Subject: [PATCH 063/160] Populate items with correct data from Kitsu or project defaults If data doesn't exist in Kitsu, use Projects default data. --- .../modules/kitsu/utils/update_op_with_zou.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 5af3a61e81..554d90f7a9 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -69,6 +69,7 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( dbcon: AvalonMongoDB, + gazu_project: dict, project_doc: dict, entities_list: List[dict], asset_doc_ids: Dict[str, dict], @@ -119,21 +120,20 @@ def update_op_assets( # because of zou's legacy design frames_duration = int(item.get("nb_frames", 0)) except (TypeError, ValueError): - frames_duration = 0 + frames_duration = None # Frame out, fallback on frame_in + duration or project's value or 1001 frame_out = item_data.pop("frame_out", None) if not frame_out: - frame_out = frame_in + frames_duration - try: - frame_out = int(frame_out) - except (TypeError, ValueError): - frame_out = 1001 + if frames_duration: + frame_out = frame_in + frames_duration + else: + frame_out = project_doc["data"].get("frameEnd", 1001) item_data["frameEnd"] = frame_out # Fps, fallback to project's value or default value (25.0) try: - fps = float(item_data.get("fps", project_doc["data"].get("fps"))) + fps = float(item_data.get("fps")) except (TypeError, ValueError): - fps = 25.0 + fps = float(gazu_project.get("fps", project_doc["data"].get("fps", 25))) item_data["fps"] = fps # Tasks @@ -424,7 +424,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, project_doc, all_entities, zou_ids_and_asset_docs + dbcon, project, project_doc, all_entities, zou_ids_and_asset_docs ) ] ) From a005d108292821e9a062da8f3cef32f113ad8eca Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Feb 2023 16:07:45 +0100 Subject: [PATCH 064/160] Add Resolution and Pixel Aspect to each item Without this info eg. Fusion would throw an error and stop working. --- openpype/modules/kitsu/utils/update_op_with_zou.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 554d90f7a9..cbf7afb413 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -135,6 +135,17 @@ def update_op_assets( except (TypeError, ValueError): fps = float(gazu_project.get("fps", project_doc["data"].get("fps", 25))) item_data["fps"] = fps + # Resolution, fall back to project default + match_res = re.match(r"(\d+)x(\d+)", item_data.get("resolution", gazu_project.get("resolution"))) + if match_res: + item_data["resolutionWidth"] = int(match_res.group(1)) + item_data["resolutionHeight"] = int(match_res.group(2)) + else: + item_data["resolutionWidth"] = project_doc["data"].get("resolutionWidth") + item_data["resolutionHeight"] = project_doc["data"].get("resolutionHeight") + # Properties that doesn't fully exist in Kitsu. Guessing the property name + # Pixel Aspect Ratio + item_data["pixelAspect"] = item_data.get("pixel_aspect", project_doc["data"].get("pixelAspect")) # Tasks tasks_list = [] From 3b4cdb13db5c3ae89b5c3e3bd6a1b0e73731fe3d Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Feb 2023 20:13:00 +0100 Subject: [PATCH 065/160] Add parents key to assets and shots Without this you can't open the project in Tray Publisher --- openpype/modules/kitsu/utils/update_op_with_zou.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index cbf7afb413..1b191ccb1e 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -401,6 +401,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): "root_of": r, "tasks": {}, "visualParent": None, + "parents": [], }, } for r in ["Assets", "Shots"] From 5dee2b8ff6b8eedbaaf68af164b316874f413059 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Feb 2023 17:15:55 +0100 Subject: [PATCH 066/160] Add missing attributes to each asset Without them, Tray Publisher errors out. --- openpype/modules/kitsu/utils/update_op_with_zou.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 1b191ccb1e..d88198eace 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -146,6 +146,14 @@ def update_op_assets( # Properties that doesn't fully exist in Kitsu. Guessing the property name # Pixel Aspect Ratio item_data["pixelAspect"] = item_data.get("pixel_aspect", project_doc["data"].get("pixelAspect")) + # Handle Start + item_data["handleStart"] = item_data.get("handle_start", project_doc["data"].get("handleStart")) + # Handle End + item_data["handleEnd"] = item_data.get("handle_end", project_doc["data"].get("handleEnd")) + # Clip In + item_data["clipIn"] = item_data.get("clip_in", project_doc["data"].get("clipIn")) + # Clip Out + item_data["clipOut"] = item_data.get("clip_out", project_doc["data"].get("clipOut")) # Tasks tasks_list = [] From 785825751a0e5dacdc6e7bf2825e8360456d906c Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Feb 2023 17:18:16 +0100 Subject: [PATCH 067/160] Add render to families to make sure a task exists to process --- openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py | 2 +- .../modules/kitsu/plugins/publish/integrate_kitsu_review.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index ea98e0b7cc..b801e0e4d4 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -8,7 +8,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" - # families = ["kitsu"] + families = ["render", "kitsu"] set_status_note = False note_status_shortname = "wfa" diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index e5e6439439..9e9eaadc27 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -8,7 +8,7 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder + 0.01 label = "Kitsu Review" - # families = ["kitsu"] + families = ["render", "kitsu"] optional = True def process(self, instance): From 27c8a1f36099347f777c538d72c67eb5dcb9dc80 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Feb 2023 19:31:06 +0100 Subject: [PATCH 068/160] Fixed so correct review file gets uploaded to Kitsu --- .../modules/kitsu/plugins/publish/integrate_kitsu_review.py | 2 +- .../projects_schema/schemas/schema_representation_tags.json | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 9e9eaadc27..94897b2553 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -27,7 +27,7 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): # Add review representations as preview of comment for representation in instance.data.get("representations", []): # Skip if not tagged as review - if "review" not in representation.get("tags", []): + if "kitsureview" not in representation.get("tags", []): continue review_path = representation.get("published_path") diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json index a4b28f47bc..7046952eef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_representation_tags.json @@ -16,6 +16,9 @@ { "shotgridreview": "Add review to Shotgrid" }, + { + "kitsureview": "Add review to Kitsu" + }, { "delete": "Delete output" }, From 72270005edc887e8e708a8d078f8a3d7ac1ec287 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Feb 2023 23:33:57 +0100 Subject: [PATCH 069/160] Update Kitsu plugin to work without assetEntry --- .../plugins/publish/collect_kitsu_entities.py | 83 +++++++++++-------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index c9e78b59eb..38c67898ef 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -4,6 +4,11 @@ import os import gazu import pyblish.api +from openpype.client import ( + get_projects, + get_project, + get_assets, +) class CollectKitsuEntities(pyblish.api.ContextPlugin): """Collect Kitsu entities according to the current context""" @@ -12,20 +17,34 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): label = "Kitsu entities" def process(self, context): + + # Get all needed names + project_name = context.data.get("projectName") + asset_name = context.data.get("asset") + task_name = context.data.get("task") + # If asset and task name doesn't exist in context, look in instance + for instance in context: + if not asset_name: + asset_name = instance.data.get("asset") + if not task_name: + task_name = instance.data.get("task") - asset_data = context.data["assetEntity"]["data"] - zou_asset_data = asset_data.get("zou") + # Get all assets of the local project + asset_docs = { + asset_doc["name"]: asset_doc + for asset_doc in get_assets(project_name) + } + + # Get asset object + asset = asset_docs.get(asset_name) + if not asset: + raise AssertionError("{} not found in DB".format(asset_name)) + + zou_asset_data = asset["data"].get("zou") if not zou_asset_data: raise AssertionError("Zou asset data not found in OpenPype!") self.log.debug("Collected zou asset data: {}".format(zou_asset_data)) - zou_task_data = asset_data["tasks"][os.environ["AVALON_TASK"]].get( - "zou" - ) - if not zou_task_data: - self.log.warning("Zou task data not found in OpenPype!") - self.log.debug("Collected zou task data: {}".format(zou_task_data)) - kitsu_project = gazu.project.get_project(zou_asset_data["project_id"]) if not kitsu_project: raise AssertionError("Project not found in kitsu!") @@ -37,37 +56,33 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_entity = gazu.shot.get_shot(zou_asset_data["id"]) else: kitsu_entity = gazu.asset.get_asset(zou_asset_data["id"]) - if not kitsu_entity: raise AssertionError("{} not found in kitsu!".format(entity_type)) - context.data["kitsu_entity"] = kitsu_entity self.log.debug( "Collect kitsu {}: {}".format(entity_type, kitsu_entity) ) - if zou_task_data: - kitsu_task = gazu.task.get_task(zou_task_data["id"]) - if not kitsu_task: - raise AssertionError("Task not found in kitsu!") - context.data["kitsu_task"] = kitsu_task - self.log.debug("Collect kitsu task: {}".format(kitsu_task)) - - else: - kitsu_task_type = gazu.task.get_task_type_by_name( - os.environ["AVALON_TASK"] - ) - if not kitsu_task_type: - raise AssertionError( - "Task type {} not found in Kitsu!".format( - os.environ["AVALON_TASK"] + if task_name: + zou_task_data = asset["data"]["tasks"][task_name].get("zou") + self.log.debug("Collected zou task data: {}".format(zou_task_data)) + if zou_task_data: + kitsu_task = gazu.task.get_task(zou_task_data["id"]) + if not kitsu_task: + raise AssertionError("Task not found in kitsu!") + context.data["kitsu_task"] = kitsu_task + self.log.debug("Collect kitsu task: {}".format(kitsu_task)) + else: + kitsu_task_type = gazu.task.get_task_type_by_name(task_name) + if not kitsu_task_type: + raise AssertionError( + "Task type {} not found in Kitsu!".format(task_name) ) - ) - kitsu_task = gazu.task.get_task_by_name( - kitsu_entity, kitsu_task_type - ) - if not kitsu_task: - raise AssertionError("Task not found in kitsu!") - context.data["kitsu_task"] = kitsu_task - self.log.debug("Collect kitsu task: {}".format(kitsu_task)) + kitsu_task = gazu.task.get_task_by_name( + kitsu_entity, kitsu_task_type + ) + if not kitsu_task: + raise AssertionError("Task not found in kitsu!") + context.data["kitsu_task"] = kitsu_task + self.log.debug("Collect kitsu task: {}".format(kitsu_task)) \ No newline at end of file From 62c111e9486e532255d213d0508f00281f40e86c Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Feb 2023 00:55:01 +0100 Subject: [PATCH 070/160] Add kitsureview to default burnin tags --- openpype/settings/defaults/project_settings/global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index a5e2d25a88..aad17d54da 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -139,7 +139,8 @@ "ext": "mp4", "tags": [ "burnin", - "ftrackreview" + "ftrackreview", + "kitsureview" ], "burnins": [], "ffmpeg_args": { From dc2b519fd5689389a482c4772289af8ba9154176 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Feb 2023 01:07:48 +0100 Subject: [PATCH 071/160] Fixed hound-bots comments --- .../plugins/publish/collect_kitsu_entities.py | 25 ++++++++--------- .../modules/kitsu/utils/update_op_with_zou.py | 27 ++++++++++++------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 38c67898ef..92c8c1823d 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -5,11 +5,10 @@ import gazu import pyblish.api from openpype.client import ( - get_projects, - get_project, get_assets, ) + class CollectKitsuEntities(pyblish.api.ContextPlugin): """Collect Kitsu entities according to the current context""" @@ -17,7 +16,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): label = "Kitsu entities" def process(self, context): - + # Get all needed names project_name = context.data.get("projectName") asset_name = context.data.get("asset") @@ -38,18 +37,18 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): # Get asset object asset = asset_docs.get(asset_name) if not asset: - raise AssertionError("{} not found in DB".format(asset_name)) + raise AssertionError(f"{asset_name} not found in DB") zou_asset_data = asset["data"].get("zou") if not zou_asset_data: raise AssertionError("Zou asset data not found in OpenPype!") - self.log.debug("Collected zou asset data: {}".format(zou_asset_data)) + self.log.debug(f"Collected zou asset data: {zou_asset_data}") kitsu_project = gazu.project.get_project(zou_asset_data["project_id"]) if not kitsu_project: raise AssertionError("Project not found in kitsu!") context.data["kitsu_project"] = kitsu_project - self.log.debug("Collect kitsu project: {}".format(kitsu_project)) + self.log.debug(f"Collect kitsu project: {kitsu_project}") entity_type = zou_asset_data["type"] if entity_type == "Shot": @@ -57,26 +56,24 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): else: kitsu_entity = gazu.asset.get_asset(zou_asset_data["id"]) if not kitsu_entity: - raise AssertionError("{} not found in kitsu!".format(entity_type)) + raise AssertionError(f"{entity_type} not found in kitsu!") context.data["kitsu_entity"] = kitsu_entity - self.log.debug( - "Collect kitsu {}: {}".format(entity_type, kitsu_entity) - ) + self.log.debug(f"Collect kitsu {entity_type}: {kitsu_entity}") if task_name: zou_task_data = asset["data"]["tasks"][task_name].get("zou") - self.log.debug("Collected zou task data: {}".format(zou_task_data)) + self.log.debug(f"Collected zou task data: {zou_task_data}") if zou_task_data: kitsu_task = gazu.task.get_task(zou_task_data["id"]) if not kitsu_task: raise AssertionError("Task not found in kitsu!") context.data["kitsu_task"] = kitsu_task - self.log.debug("Collect kitsu task: {}".format(kitsu_task)) + self.log.debug(f"Collect kitsu task: {kitsu_task}") else: kitsu_task_type = gazu.task.get_task_type_by_name(task_name) if not kitsu_task_type: raise AssertionError( - "Task type {} not found in Kitsu!".format(task_name) + f"Task type {task_name} not found in Kitsu!" ) kitsu_task = gazu.task.get_task_by_name( @@ -85,4 +82,4 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): if not kitsu_task: raise AssertionError("Task not found in kitsu!") context.data["kitsu_task"] = kitsu_task - self.log.debug("Collect kitsu task: {}".format(kitsu_task)) \ No newline at end of file + self.log.debug(f"Collect kitsu task: {kitsu_task}") diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index d88198eace..a079fd5529 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -133,27 +133,36 @@ def update_op_assets( try: fps = float(item_data.get("fps")) except (TypeError, ValueError): - fps = float(gazu_project.get("fps", project_doc["data"].get("fps", 25))) + fps = float(gazu_project.get( + "fps", project_doc["data"].get("fps", 25))) item_data["fps"] = fps # Resolution, fall back to project default - match_res = re.match(r"(\d+)x(\d+)", item_data.get("resolution", gazu_project.get("resolution"))) + match_res = re.match( + r"(\d+)x(\d+)", item_data.get("resolution", gazu_project.get("resolution"))) if match_res: item_data["resolutionWidth"] = int(match_res.group(1)) item_data["resolutionHeight"] = int(match_res.group(2)) else: - item_data["resolutionWidth"] = project_doc["data"].get("resolutionWidth") - item_data["resolutionHeight"] = project_doc["data"].get("resolutionHeight") + item_data["resolutionWidth"] = project_doc["data"].get( + "resolutionWidth") + item_data["resolutionHeight"] = project_doc["data"].get( + "resolutionHeight") # Properties that doesn't fully exist in Kitsu. Guessing the property name # Pixel Aspect Ratio - item_data["pixelAspect"] = item_data.get("pixel_aspect", project_doc["data"].get("pixelAspect")) + item_data["pixelAspect"] = item_data.get( + "pixel_aspect", project_doc["data"].get("pixelAspect")) # Handle Start - item_data["handleStart"] = item_data.get("handle_start", project_doc["data"].get("handleStart")) + item_data["handleStart"] = item_data.get( + "handle_start", project_doc["data"].get("handleStart")) # Handle End - item_data["handleEnd"] = item_data.get("handle_end", project_doc["data"].get("handleEnd")) + item_data["handleEnd"] = item_data.get( + "handle_end", project_doc["data"].get("handleEnd")) # Clip In - item_data["clipIn"] = item_data.get("clip_in", project_doc["data"].get("clipIn")) + item_data["clipIn"] = item_data.get( + "clip_in", project_doc["data"].get("clipIn")) # Clip Out - item_data["clipOut"] = item_data.get("clip_out", project_doc["data"].get("clipOut")) + item_data["clipOut"] = item_data.get( + "clip_out", project_doc["data"].get("clipOut")) # Tasks tasks_list = [] From bb61d43c27286bda8b319d5e49da8166c1472ae8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Feb 2023 11:52:37 +0100 Subject: [PATCH 072/160] Shortened length of line 141 --- openpype/modules/kitsu/utils/update_op_with_zou.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a079fd5529..15e88947a1 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -138,7 +138,9 @@ def update_op_assets( item_data["fps"] = fps # Resolution, fall back to project default match_res = re.match( - r"(\d+)x(\d+)", item_data.get("resolution", gazu_project.get("resolution"))) + r"(\d+)x(\d+)", + item_data.get("resolution", gazu_project.get("resolution")) + ) if match_res: item_data["resolutionWidth"] = int(match_res.group(1)) item_data["resolutionHeight"] = int(match_res.group(2)) From 47caa760305dcb46639d94ae7c345d8682e724fc Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Feb 2023 13:32:03 +0100 Subject: [PATCH 073/160] Updated Kitsu Sync module to fully work with all events Manually tried all events, added logging for events and cleaned up the code some. --- openpype/modules/kitsu/utils/sync_service.py | 318 +++++++++++++----- .../modules/kitsu/utils/update_op_with_zou.py | 17 +- 2 files changed, 248 insertions(+), 87 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 237746bea0..e371c1a9bb 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -1,3 +1,15 @@ +""" +Bugs: + * Error when adding task type to anything that isn't Shot or Assets + * Assets don't get added under an episode if TV show + * Assets added under Main Pack throws error. Can't get the name of Main Pack? + +Features ToDo: + * Select in settings what types you wish to sync + * Print what's updated on entity-update + * Add listener for Edits +""" + import os import threading @@ -5,6 +17,7 @@ import gazu from openpype.client import get_project, get_assets, get_asset_by_name from openpype.pipeline import AvalonMongoDB +from openpype.lib import Logger from .credentials import validate_credentials from .update_op_with_zou import ( create_op_asset, @@ -14,6 +27,8 @@ from .update_op_with_zou import ( update_op_assets, ) +log = Logger.get_logger(__name__) + class Listener: """Host Kitsu listener.""" @@ -33,7 +48,7 @@ class Listener: self.dbcon = AvalonMongoDB() self.dbcon.install() - gazu.client.set_host(os.environ["KITSU_SERVER"]) + gazu.client.set_host(os.environ['KITSU_SERVER']) # Authenticate if not validate_credentials(login, password): @@ -42,7 +57,7 @@ class Listener: ) gazu.set_event_host( - os.environ["KITSU_SERVER"].replace("api", "socket.io") + os.environ['KITSU_SERVER'].replace("api", "socket.io") ) self.event_client = gazu.events.init() @@ -103,6 +118,8 @@ class Listener: ) def start(self): + """Start listening for events.""" + log.info("Listening to Kitsu events...") gazu.events.run_client(self.event_client) # == Project == @@ -112,36 +129,49 @@ class Listener: # Use update process to avoid duplicating code self._update_project(data) + # Print message + ## Happens in write_project_to_op() + def _update_project(self, data): """Update project into OP DB.""" # Get project entity - project = gazu.project.get_project(data["project_id"]) - project_name = project["name"] + project = gazu.project.get_project(data['project_id']) update_project = write_project_to_op(project, self.dbcon) # Write into DB if update_project: - self.dbcon.Session["AVALON_PROJECT"] = project_name + self.dbcon.Session['AVALON_PROJECT'] = get_kitsu_project_name( + data['project_id']) self.dbcon.bulk_write([update_project]) def _delete_project(self, data): """Delete project.""" - project_name = get_kitsu_project_name(data["project_id"]) + collections = self.dbcon.database.list_collection_names() + project_name = None + for collection in collections: + post = self.dbcon.database[collection].find_one( + {"data.zou_id": data['project_id']}) + if post: + project_name = post['name'] + break - # Delete project collection - self.dbcon.database[project_name].drop() + if project_name: + # Delete project collection + self.dbcon.database[project_name].drop() + + # Print message + log.info(f"Project deleted: {project_name}") # == Asset == - def _new_asset(self, data): """Create new asset into OP DB.""" # Get project entity - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) - # Get gazu entity - asset = gazu.asset.get_asset(data["asset_id"]) + # Get asset entity + asset = gazu.asset.get_asset(data['asset_id']) # Insert doc in DB self.dbcon.insert_one(create_op_asset(asset)) @@ -149,27 +179,43 @@ class Listener: # Update self._update_asset(data) + # Print message + episode = None + ep_id = asset['episode_id'] + if ep_id and ep_id != "": + episode = gazu.asset.get_episode(ep_id) + + msg = "Asset created: " + msg = msg + f"{asset['project_name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{asset['asset_type_name']}_" + msg = msg + f"{asset['name']}" + log.info(msg) + def _update_asset(self, data): """Update asset into OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - asset = gazu.asset.get_asset(data["asset_id"]) + asset = gazu.asset.get_asset(data['asset_id']) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc["data"]["zou"]["id"]: asset_doc + asset_doc['data']['zou']['id']: asset_doc for asset_doc in get_assets(project_name) - if asset_doc["data"].get("zou", {}).get("id") + if asset_doc['data'].get("zou", {}).get("id") } - zou_ids_and_asset_docs[asset["project_id"]] = project_doc + zou_ids_and_asset_docs[asset['project_id']] = project_doc + gazu_project = gazu.project.get_project(asset['project_id']) # Update update_op_result = update_op_assets( - self.dbcon, project_doc, [asset], zou_ids_and_asset_docs + self.dbcon, gazu_project, project_doc, + [asset], zou_ids_and_asset_docs ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -177,21 +223,37 @@ class Listener: def _delete_asset(self, data): """Delete asset of OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) - # Delete - self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data["asset_id"]} - ) + asset = self.dbcon.find_one({"data.zou.id": data['asset_id']}) + if asset: + # Delete + self.dbcon.delete_one( + {"type": "asset", "data.zou.id": data['asset_id']} + ) + + # Print message + episode = None + ep_id = asset['data']['zou']['episode_id'] + if ep_id and ep_id != "": + episode = gazu.asset.get_episode(ep_id) + + msg = "Asset deleted: " + msg = msg + f"{asset['data']['zou']['project_name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{asset['data']['zou']['asset_type_name']}_" + msg = msg + f"'{asset['name']}" + log.info(msg) # == Episode == def _new_episode(self, data): """Create new episode into OP DB.""" # Get project entity - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) # Get gazu entity - episode = gazu.shot.get_episode(data["episode_id"]) + episode = gazu.shot.get_episode(data['episode_id']) # Insert doc in DB self.dbcon.insert_one(create_op_asset(episode)) @@ -199,27 +261,34 @@ class Listener: # Update self._update_episode(data) + # Print message + msg = "Episode created: " + msg = msg + f"{episode['project_name']} - " + msg = msg + f"{episode['name']}" + def _update_episode(self, data): """Update episode into OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - episode = gazu.shot.get_episode(data["episode_id"]) + episode = gazu.shot.get_episode(data['episode_id']) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc["data"]["zou"]["id"]: asset_doc + asset_doc['data']['zou']['id']: asset_doc for asset_doc in get_assets(project_name) - if asset_doc["data"].get("zou", {}).get("id") + if asset_doc['data'].get("zou", {}).get("id") } - zou_ids_and_asset_docs[episode["project_id"]] = project_doc + zou_ids_and_asset_docs[episode['project_id']] = project_doc + gazu_project = gazu.project.get_project(episode['project_id']) # Update update_op_result = update_op_assets( - self.dbcon, project_doc, [episode], zou_ids_and_asset_docs + self.dbcon, gazu_project, project_doc, [ + episode], zou_ids_and_asset_docs ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -227,22 +296,31 @@ class Listener: def _delete_episode(self, data): """Delete shot of OP DB.""" - set_op_project(self.dbcon, data["project_id"]) - print("delete episode") # TODO check bugfix + set_op_project(self.dbcon, data['project_id']) - # Delete - self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data["episode_id"]} - ) + episode = self.dbcon.find_one({"data.zou.id": data['episode_id']}) + if episode: + # Delete + self.dbcon.delete_one( + {"type": "asset", "data.zou.id": data['episode_id']} + ) + + # Print message + project = gazu.project.get_project( + episode['data']['zou']['project_id']) + + msg = "Episode deleted: " + msg = msg + f"{project['name']} - " + msg = msg + f"{episode['name']}" # == Sequence == def _new_sequence(self, data): """Create new sequnce into OP DB.""" # Get project entity - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) # Get gazu entity - sequence = gazu.shot.get_sequence(data["sequence_id"]) + sequence = gazu.shot.get_sequence(data['sequence_id']) # Insert doc in DB self.dbcon.insert_one(create_op_asset(sequence)) @@ -250,27 +328,43 @@ class Listener: # Update self._update_sequence(data) + # Print message + + episode = None + ep_id = sequence['episode_id'] + if ep_id and ep_id != "": + episode = gazu.asset.get_episode(ep_id) + + msg = "Sequence created: " + msg = msg + f"{sequence['project_name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{sequence['name']}" + log.info(msg) + def _update_sequence(self, data): """Update sequence into OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - sequence = gazu.shot.get_sequence(data["sequence_id"]) + sequence = gazu.shot.get_sequence(data['sequence_id']) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc["data"]["zou"]["id"]: asset_doc + asset_doc['data']['zou']['id']: asset_doc for asset_doc in get_assets(project_name) - if asset_doc["data"].get("zou", {}).get("id") + if asset_doc['data'].get("zou", {}).get("id") } - zou_ids_and_asset_docs[sequence["project_id"]] = project_doc + zou_ids_and_asset_docs[sequence['project_id']] = project_doc + gazu_project = gazu.project.get_project(sequence['project_id']) # Update update_op_result = update_op_assets( - self.dbcon, project_doc, [sequence], zou_ids_and_asset_docs + self.dbcon, gazu_project, project_doc, + [sequence], zou_ids_and_asset_docs ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -278,22 +372,30 @@ class Listener: def _delete_sequence(self, data): """Delete sequence of OP DB.""" - set_op_project(self.dbcon, data["project_id"]) - print("delete sequence") # TODO check bugfix + set_op_project(self.dbcon, data['project_id']) + sequence = self.dbcon.find_one({"data.zou.id": data['sequence_id']}) + if sequence: + # Delete + self.dbcon.delete_one( + {"type": "asset", "data.zou.id": data['sequence_id']} + ) - # Delete - self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data["sequence_id"]} - ) + # Print message + gazu_project = gazu.project.get_project( + sequence['data']['zou']['project_id']) + msg = f"Sequence deleted: " + msg = msg + f"{gazu_project['name']} - " + msg = msg + f"{sequence['name']}" + log.info(msg) # == Shot == def _new_shot(self, data): """Create new shot into OP DB.""" # Get project entity - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) # Get gazu entity - shot = gazu.shot.get_shot(data["shot_id"]) + shot = gazu.shot.get_shot(data['shot_id']) # Insert doc in DB self.dbcon.insert_one(create_op_asset(shot)) @@ -301,89 +403,151 @@ class Listener: # Update self._update_shot(data) + # Print message + episode = None + if shot['episode_id'] and shot['episode_id'] != "": + episode = gazu.asset.get_episode(shot['episode_id']) + + msg = "Shot created: " + msg = msg + f"{shot['project_name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{shot['sequence_name']}_" + msg = msg + f"{shot['name']}" + log.info(msg) + def _update_shot(self, data): """Update shot into OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - shot = gazu.shot.get_shot(data["shot_id"]) + shot = gazu.shot.get_shot(data['shot_id']) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc["data"]["zou"]["id"]: asset_doc + asset_doc['data']['zou']['id']: asset_doc for asset_doc in get_assets(project_name) - if asset_doc["data"].get("zou", {}).get("id") + if asset_doc['data'].get("zou", {}).get("id") } - zou_ids_and_asset_docs[shot["project_id"]] = project_doc + zou_ids_and_asset_docs[shot['project_id']] = project_doc + gazu_project = gazu.project.get_project(shot['project_id']) # Update update_op_result = update_op_assets( - self.dbcon, project_doc, [shot], zou_ids_and_asset_docs + self.dbcon, gazu_project, project_doc, + [shot], zou_ids_and_asset_docs ) + if update_op_result: asset_doc_id, asset_update = update_op_result[0] self.dbcon.update_one({"_id": asset_doc_id}, asset_update) def _delete_shot(self, data): """Delete shot of OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) + shot = self.dbcon.find_one({"data.zou.id": data['shot_id']}) - # Delete - self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data["shot_id"]} - ) + if shot: + # Delete + self.dbcon.delete_one( + {"type": "asset", "data.zou.id": data['shot_id']} + ) + + # Print message + gazu_project = gazu.project.get_project( + shot['data']['zou']['project_id']) + + msg = "Shot deleted: " + msg = msg + f"{gazu_project['name']} - " + msg = msg + f"{shot['name']}" + log.info(msg) # == Task == def _new_task(self, data): """Create new task into OP DB.""" # Get project entity - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) project_name = self.dbcon.active_project() # Get gazu entity - task = gazu.task.get_task(data["task_id"]) + task = gazu.task.get_task(data['task_id']) # Find asset doc - parent_name = task["entity"]["name"] + episode = None + ep_id = task['episode_id'] + if ep_id and ep_id != "": + episode = gazu.asset.get_episode(ep_id) + + parent_name = "" + if episode is not None: + parent_name = episode['name'] + "_" + parent_name = parent_name + \ + task['sequence']['name'] + "_" + task['entity']['name'] asset_doc = get_asset_by_name(project_name, parent_name) # Update asset tasks with new one - asset_tasks = asset_doc["data"].get("tasks") - task_type_name = task["task_type"]["name"] + asset_tasks = asset_doc['data'].get("tasks") + task_type_name = task['task_type']['name'] asset_tasks[task_type_name] = {"type": task_type_name, "zou": task} self.dbcon.update_one( - {"_id": asset_doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} + {"_id": asset_doc['_id']}, {"$set": {"data.tasks": asset_tasks}} ) + # Print message + msg = "Task created: " + msg = msg + f"{task['project']['name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{task['sequence']['name']}_" + msg = msg + f"{task['entity']['name']} - " + msg = msg + f"{task['task_type']['name']}" + log.info(msg) + def _update_task(self, data): """Update task into OP DB.""" # TODO is it necessary? - pass def _delete_task(self, data): """Delete task of OP DB.""" - set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data['project_id']) project_name = self.dbcon.active_project() # Find asset doc asset_docs = list(get_assets(project_name)) for doc in asset_docs: # Match task - for name, task in doc["data"]["tasks"].items(): - if task.get("zou") and data["task_id"] == task["zou"]["id"]: + for name, task in doc['data']['tasks'].items(): + if task.get("zou") and data['task_id'] == task['zou']['id']: # Pop task - asset_tasks = doc["data"].get("tasks", {}) + asset_tasks = doc['data'].get("tasks", {}) asset_tasks.pop(name) # Delete task in DB self.dbcon.update_one( - {"_id": doc["_id"]}, + {"_id": doc['_id']}, {"$set": {"data.tasks": asset_tasks}}, ) + + # Print message + shot = gazu.shot.get_shot(task['zou']['entity_id']) + + episode = None + ep_id = shot['episode_id'] + if ep_id and ep_id != "": + episode = gazu.asset.get_episode(ep_id) + + msg = "Task deleted: " + msg = msg + f"{shot['project_name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{shot['sequence_name']}_" + msg = msg + f"{shot['name']} - " + msg = msg + f"{task['type']}" + log.info(msg) return @@ -396,7 +560,7 @@ def start_listeners(login: str, password: str): """ # Refresh token every week def refresh_token_every_week(): - print("Refreshing token...") + log.info("Refreshing token...") gazu.refresh_token() threading.Timer(7 * 3600 * 24, refresh_token_every_week).start() diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 15e88947a1..0a59724393 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -5,10 +5,6 @@ from typing import Dict, List from pymongo import DeleteOne, UpdateOne import gazu -from gazu.task import ( - all_tasks_for_asset, - all_tasks_for_shot, -) from openpype.client import ( get_project, @@ -18,7 +14,6 @@ from openpype.client import ( create_project, ) from openpype.pipeline import AvalonMongoDB -from openpype.settings import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials from openpype.lib import Logger @@ -85,8 +80,10 @@ def update_op_assets( Returns: List[Dict[str, dict]]: List of (doc_id, update_dict) tuples """ + if not project_doc: + return + project_name = project_doc["name"] - project_module_settings = get_project_settings(project_name)["kitsu"] assets_with_update = [] for item in entities_list: @@ -170,9 +167,9 @@ def update_op_assets( tasks_list = [] item_type = item["type"] if item_type == "Asset": - tasks_list = all_tasks_for_asset(item) + tasks_list = gazu.task.all_tasks_for_asset(item) elif item_type == "Shot": - tasks_list = all_tasks_for_shot(item) + tasks_list = gazu.task.all_tasks_for_shot(item) item_data["tasks"] = { t["task_type_name"]: {"type": t["task_type_name"], "zou": t} for t in tasks_list @@ -207,7 +204,7 @@ def update_op_assets( # Root parent folder if exist visual_parent_doc_id = ( - asset_doc_ids[parent_zou_id]["_id"] if parent_zou_id else None + asset_doc_ids[parent_zou_id].get("_id") if parent_zou_id else None ) if visual_parent_doc_id is None: # Find root folder doc ("Assets" or "Shots") @@ -282,7 +279,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: project_name = project["name"] project_doc = get_project(project_name) if not project_doc: - log.info(f"Creating project '{project_name}'") + log.info(f"Project created: {project_name}") project_doc = create_project(project_name, project_name) # Project data and tasks From c44c5c9ddbada1264691a32c78cf1765b0b9bce1 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Feb 2023 14:08:33 +0100 Subject: [PATCH 074/160] Get asset and task names from instance only --- .../kitsu/plugins/publish/collect_kitsu_entities.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 92c8c1823d..5499b1782a 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -19,14 +19,12 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): # Get all needed names project_name = context.data.get("projectName") - asset_name = context.data.get("asset") - task_name = context.data.get("task") + asset_name = None + task_name = None # If asset and task name doesn't exist in context, look in instance for instance in context: - if not asset_name: - asset_name = instance.data.get("asset") - if not task_name: - task_name = instance.data.get("task") + asset_name = instance.data.get("asset") + task_name = instance.data.get("task") # Get all assets of the local project asset_docs = { From 4bc67437b61326540a2a62f141af5169296ee051 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Feb 2023 14:36:17 +0100 Subject: [PATCH 075/160] Fixed line too long and too many '#' for comments --- openpype/modules/kitsu/utils/sync_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index e371c1a9bb..00c8c4eafa 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -2,7 +2,7 @@ Bugs: * Error when adding task type to anything that isn't Shot or Assets * Assets don't get added under an episode if TV show - * Assets added under Main Pack throws error. Can't get the name of Main Pack? + * Assets added under Main Pack throws error. No Main Pack name in dict Features ToDo: * Select in settings what types you wish to sync @@ -130,7 +130,7 @@ class Listener: self._update_project(data) # Print message - ## Happens in write_project_to_op() + # - Happens in write_project_to_op() def _update_project(self, data): """Update project into OP DB.""" From 8652dab47898e30cbdeb9c3ed9ced2f841c4ec4c Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 13:14:14 +0100 Subject: [PATCH 076/160] Fixed 'instance' code --- .../plugins/publish/collect_kitsu_entities.py | 104 ++++++++---------- .../plugins/publish/integrate_kitsu_note.py | 48 ++++---- .../plugins/publish/integrate_kitsu_review.py | 6 +- 3 files changed, 73 insertions(+), 85 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 5499b1782a..fe6854218d 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -1,13 +1,7 @@ # -*- coding: utf-8 -*- -import os - import gazu import pyblish.api -from openpype.client import ( - get_assets, -) - class CollectKitsuEntities(pyblish.api.ContextPlugin): """Collect Kitsu entities according to the current context""" @@ -17,67 +11,61 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): def process(self, context): - # Get all needed names - project_name = context.data.get("projectName") - asset_name = None - task_name = None - # If asset and task name doesn't exist in context, look in instance + kitsu_project = None + + kitsu_entities_by_id = {} for instance in context: - asset_name = instance.data.get("asset") + asset_doc = instance.data.get("assetEntity") task_name = instance.data.get("task") + if not asset_doc: + continue - # Get all assets of the local project - asset_docs = { - asset_doc["name"]: asset_doc - for asset_doc in get_assets(project_name) - } + zou_asset_data = asset_doc["data"].get("zou") + if not zou_asset_data: + raise AssertionError("Zou asset data not found in OpenPype!") - # Get asset object - asset = asset_docs.get(asset_name) - if not asset: - raise AssertionError(f"{asset_name} not found in DB") + if kitsu_project is None: + kitsu_project = gazu.project.get_project( + zou_asset_data["project_id"]) + if not kitsu_project: + raise AssertionError("Project not found in kitsu!") - zou_asset_data = asset["data"].get("zou") - if not zou_asset_data: - raise AssertionError("Zou asset data not found in OpenPype!") - self.log.debug(f"Collected zou asset data: {zou_asset_data}") + entity_type = zou_asset_data["type"] + kitsu_id = zou_asset_data["id"] + kitsu_entity = kitsu_entities_by_id.get(kitsu_id) + if not kitsu_entity: + if entity_type == "Shot": + kitsu_entity = gazu.shot.get_shot(kitsu_id) + else: + kitsu_entity = gazu.asset.get_asset(kitsu_id) + kitsu_entities_by_id[kitsu_id] = kitsu_entity - kitsu_project = gazu.project.get_project(zou_asset_data["project_id"]) - if not kitsu_project: - raise AssertionError("Project not found in kitsu!") - context.data["kitsu_project"] = kitsu_project - self.log.debug(f"Collect kitsu project: {kitsu_project}") + if not kitsu_entity: + raise AssertionError( + "{} not found in kitsu!".format(entity_type)) + instance.data["kitsu_entity"] = kitsu_entity - entity_type = zou_asset_data["type"] - if entity_type == "Shot": - kitsu_entity = gazu.shot.get_shot(zou_asset_data["id"]) - else: - kitsu_entity = gazu.asset.get_asset(zou_asset_data["id"]) - if not kitsu_entity: - raise AssertionError(f"{entity_type} not found in kitsu!") - context.data["kitsu_entity"] = kitsu_entity - self.log.debug(f"Collect kitsu {entity_type}: {kitsu_entity}") - - if task_name: - zou_task_data = asset["data"]["tasks"][task_name].get("zou") - self.log.debug(f"Collected zou task data: {zou_task_data}") - if zou_task_data: - kitsu_task = gazu.task.get_task(zou_task_data["id"]) - if not kitsu_task: - raise AssertionError("Task not found in kitsu!") - context.data["kitsu_task"] = kitsu_task - self.log.debug(f"Collect kitsu task: {kitsu_task}") - else: + if not task_name: + continue + zou_task_data = asset_doc["data"]["tasks"][task_name].get("zou") + self.log.debug( + "Collected zou task data: {}".format(zou_task_data)) + if not zou_task_data: kitsu_task_type = gazu.task.get_task_type_by_name(task_name) if not kitsu_task_type: raise AssertionError( - f"Task type {task_name} not found in Kitsu!" + "Task type {} not found in Kitsu!".format(task_name) ) + continue + kitsu_task_id = zou_task_data["id"] + kitsu_task = kitsu_entities_by_id.get(kitsu_task_id) + if not kitsu_task: + kitsu_task = gazu.task.get_task(zou_task_data["id"]) + kitsu_entities_by_id[kitsu_task_id] = kitsu_task - kitsu_task = gazu.task.get_task_by_name( - kitsu_entity, kitsu_task_type - ) - if not kitsu_task: - raise AssertionError("Task not found in kitsu!") - context.data["kitsu_task"] = kitsu_task - self.log.debug(f"Collect kitsu task: {kitsu_task}") + if not kitsu_task: + raise AssertionError("Task not found in kitsu!") + instance.data["kitsu_task"] = kitsu_task + self.log.debug("Collect kitsu task: {}".format(kitsu_task)) + + context.data["kitsu_project"] = kitsu_project diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index b801e0e4d4..aeec2481e0 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -21,30 +21,32 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Comment is `{}`".format(publish_comment)) - # Get note status, by default uses the task status for the note - # if it is not specified in the configuration - note_status = context.data["kitsu_task"]["task_status_id"] - if self.set_status_note: - kitsu_status = gazu.task.get_task_status_by_short_name( - self.note_status_shortname - ) - if kitsu_status: - note_status = kitsu_status - self.log.info("Note Kitsu status: {}".format(note_status)) - else: - self.log.info( - "Cannot find {} status. The status will not be " - "changed!".format(self.note_status_shortname) + for instance in context: + + # Get note status, by default uses the task status for the note + # if it is not specified in the configuration + note_status = instance.data["kitsu_task"]["task_status"]["id"] + + if self.set_status_note: + kitsu_status = gazu.task.get_task_status_by_short_name( + self.note_status_shortname ) + if kitsu_status: + note_status = kitsu_status + self.log.info("Note Kitsu status: {}".format(note_status)) + else: + self.log.info( + "Cannot find {} status. The status will not be " + "changed!".format(self.note_status_shortname) + ) - # Add comment to kitsu task - self.log.debug( - "Add new note in taks id {}".format( - context.data["kitsu_task"]["id"] + # Add comment to kitsu task + task = instance.data["kitsu_task"]["id"] + self.log.debug( + "Add new note in taks id {}".format(task) + ) + kitsu_comment = gazu.task.add_comment( + task, note_status, comment=publish_comment ) - ) - kitsu_comment = gazu.task.add_comment( - context.data["kitsu_task"], note_status, comment=publish_comment - ) - context.data["kitsu_comment"] = kitsu_comment + instance.data["kitsu_comment"] = kitsu_comment diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 94897b2553..d8f6cb7ac8 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -13,9 +13,8 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): def process(self, instance): - context = instance.context - task = context.data["kitsu_task"] - comment = context.data.get("kitsu_comment") + task = instance.data["kitsu_task"]["id"] + comment = instance.data["kitsu_comment"]["id"] # Check comment has been created if not comment: @@ -29,7 +28,6 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): # Skip if not tagged as review if "kitsureview" not in representation.get("tags", []): continue - review_path = representation.get("published_path") self.log.debug("Found review at: {}".format(review_path)) From 290709705e2811c1a87a5f18b859b5880bd09772 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:02:31 +0100 Subject: [PATCH 077/160] Changed logging from f-string to .format() --- .../modules/kitsu/actions/launcher_show_in_kitsu.py | 12 ++++++------ openpype/modules/kitsu/utils/credentials.py | 2 +- openpype/modules/kitsu/utils/sync_service.py | 2 +- openpype/modules/kitsu/utils/update_op_with_zou.py | 4 ++-- openpype/modules/kitsu/utils/update_zou_with_op.py | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 4793d60fc3..e3676afc4c 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -32,12 +32,12 @@ class ShowInKitsu(LauncherAction): project = get_project(project_name=project_name, fields=["data.zou_id"]) if not project: - raise RuntimeError(f"Project {project_name} not found.") + raise RuntimeError("Project {} not found.".format(project_name)) project_zou_id = project["data"].get("zou_id") if not project_zou_id: - raise RuntimeError(f"Project {project_name} has no " - f"connected kitsu id.") + raise RuntimeError( + "Project {} has no connected kitsu id.".format(project_name)) asset_zou_name = None asset_zou_id = None @@ -48,7 +48,7 @@ class ShowInKitsu(LauncherAction): asset_zou_name = asset_name asset_fields = ["data.zou.id", "data.zou.type"] if task_name: - asset_fields.append(f"data.tasks.{task_name}.zou.id") + asset_fields.append("data.tasks.{}.zou.id".format(task_name)) asset = get_asset_by_name(project_name, asset_name=asset_name, @@ -67,7 +67,7 @@ class ShowInKitsu(LauncherAction): task_data = asset["data"]["tasks"][task_name] task_zou_data = task_data.get("zou", {}) if not task_zou_data: - self.log.debug(f"No zou task data for task: {task_name}") + self.log.debug("No zou task data for task: {}".format(task_name)) task_zou_id = task_zou_data["id"] # Define URL @@ -78,7 +78,7 @@ class ShowInKitsu(LauncherAction): task_id=task_zou_id) # Open URL in webbrowser - self.log.info(f"Opening URL: {url}") + self.log.info("Opening URL: {}".format(url)) webbrowser.open(url, # Try in new tab new=2) diff --git a/openpype/modules/kitsu/utils/credentials.py b/openpype/modules/kitsu/utils/credentials.py index adcfb07cd5..1731e1ca4f 100644 --- a/openpype/modules/kitsu/utils/credentials.py +++ b/openpype/modules/kitsu/utils/credentials.py @@ -54,7 +54,7 @@ def validate_host(kitsu_url: str) -> bool: if gazu.client.host_is_valid(): return True else: - raise gazu.exception.HostException(f"Host '{kitsu_url}' is invalid.") + raise gazu.exception.HostException("Host '{}' is invalid.".format(kitsu_url)) def clear_credentials(): diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 00c8c4eafa..d4ef5ce63a 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -162,7 +162,7 @@ class Listener: self.dbcon.database[project_name].drop() # Print message - log.info(f"Project deleted: {project_name}") + log.info("Project deleted: {}".format(project_name)) # == Asset == def _new_asset(self, data): diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 0a59724393..15e6dd70d9 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -279,7 +279,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: project_name = project["name"] project_doc = get_project(project_name) if not project_doc: - log.info(f"Project created: {project_name}") + log.info("Project created: {}".format(project_name)) project_doc = create_project(project_name, project_name) # Project data and tasks @@ -373,7 +373,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): if not project: project = gazu.project.get_project_by_name(project["name"]) - log.info(f"Synchronizing {project['name']}...") + log.info("Synchronizing {}...".format(project['name'])) # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 39baf31b93..b13c2dd4c6 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -61,7 +61,7 @@ def sync_zou_from_op_project( project_doc = get_project(project_name) # Get all entities from zou - print(f"Synchronizing {project_name}...") + print("Synchronizing {}...".format(project_name)) zou_project = gazu.project.get_project_by_name(project_name) # Create project @@ -258,7 +258,7 @@ def sync_zou_from_op_project( for asset_doc in asset_docs.values() } for entity_id in deleted_entities: - gazu.raw.delete(f"data/entities/{entity_id}") + gazu.raw.delete("data/entities/{}".format(entity_id)) # Write into DB if bulk_writes: From 793af30caae9782ab9cb328f7b546231f0c727df Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:02:53 +0100 Subject: [PATCH 078/160] Changed AssertionError to ValueError --- .../kitsu/plugins/publish/collect_kitsu_entities.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index fe6854218d..dc7048cf2a 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -22,13 +22,13 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): zou_asset_data = asset_doc["data"].get("zou") if not zou_asset_data: - raise AssertionError("Zou asset data not found in OpenPype!") + raise ValueError("Zou asset data not found in OpenPype!") if kitsu_project is None: kitsu_project = gazu.project.get_project( zou_asset_data["project_id"]) if not kitsu_project: - raise AssertionError("Project not found in kitsu!") + raise ValueError("Project not found in kitsu!") entity_type = zou_asset_data["type"] kitsu_id = zou_asset_data["id"] @@ -41,7 +41,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_entities_by_id[kitsu_id] = kitsu_entity if not kitsu_entity: - raise AssertionError( + raise ValueError( "{} not found in kitsu!".format(entity_type)) instance.data["kitsu_entity"] = kitsu_entity @@ -53,7 +53,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): if not zou_task_data: kitsu_task_type = gazu.task.get_task_type_by_name(task_name) if not kitsu_task_type: - raise AssertionError( + raise ValueError( "Task type {} not found in Kitsu!".format(task_name) ) continue @@ -64,7 +64,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_entities_by_id[kitsu_task_id] = kitsu_task if not kitsu_task: - raise AssertionError("Task not found in kitsu!") + raise ValueError("Task not found in kitsu!") instance.data["kitsu_task"] = kitsu_task self.log.debug("Collect kitsu task: {}".format(kitsu_task)) From 7e6c47967f42f26fd6d0f56c33a14a0e08e77906 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:04:05 +0100 Subject: [PATCH 079/160] Get episode_id using get() as ep_id might not always exist --- openpype/modules/kitsu/utils/sync_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index d4ef5ce63a..a449bf8c06 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -477,7 +477,7 @@ class Listener: # Find asset doc episode = None - ep_id = task['episode_id'] + ep_id = task.get('episode_id') if ep_id and ep_id != "": episode = gazu.asset.get_episode(ep_id) From 1045ee0c1ed5bb10c3c3d8f4e17f399a5f33c754 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:04:55 +0100 Subject: [PATCH 080/160] Check if asset_doc exist before processing it --- openpype/modules/kitsu/utils/sync_service.py | 34 ++++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index a449bf8c06..eed259cda6 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -487,25 +487,25 @@ class Listener: parent_name = parent_name + \ task['sequence']['name'] + "_" + task['entity']['name'] - asset_doc = get_asset_by_name(project_name, parent_name) - # Update asset tasks with new one - asset_tasks = asset_doc['data'].get("tasks") - task_type_name = task['task_type']['name'] - asset_tasks[task_type_name] = {"type": task_type_name, "zou": task} - self.dbcon.update_one( - {"_id": asset_doc['_id']}, {"$set": {"data.tasks": asset_tasks}} - ) + asset_doc = get_asset_by_name(project_name, parent_name) + if asset_doc: + asset_tasks = asset_doc['data'].get("tasks") + task_type_name = task['task_type']['name'] + asset_tasks[task_type_name] = {"type": task_type_name, "zou": task} + self.dbcon.update_one( + {"_id": asset_doc['_id']}, {"$set": {"data.tasks": asset_tasks}} + ) - # Print message - msg = "Task created: " - msg = msg + f"{task['project']['name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{task['sequence']['name']}_" - msg = msg + f"{task['entity']['name']} - " - msg = msg + f"{task['task_type']['name']}" - log.info(msg) + # Print message + msg = "Task created: " + msg = msg + f"{task['project']['name']} - " + if episode is not None: + msg = msg + f"{episode['name']}_" + msg = msg + f"{task['sequence']['name']}_" + msg = msg + f"{task['entity']['name']} - " + msg = msg + f"{task['task_type']['name']}" + log.info(msg) def _update_task(self, data): """Update task into OP DB.""" From ef698b4aface5ceff0ba1016a242a318853eaa2a Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:11:07 +0100 Subject: [PATCH 081/160] Fixed 1 extra frame at frameEnd Same as #4466 --- openpype/modules/kitsu/utils/update_op_with_zou.py | 6 +++--- openpype/modules/kitsu/utils/update_zou_with_op.py | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 15e6dd70d9..93d0d5e3fb 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -111,18 +111,18 @@ def update_op_assets( except (TypeError, ValueError): frame_in = 1001 item_data["frameStart"] = frame_in - # Frames duration, fallback on 0 + # Frames duration, fallback on 1 try: # NOTE nb_frames is stored directly in item # because of zou's legacy design - frames_duration = int(item.get("nb_frames", 0)) + frames_duration = int(item.get("nb_frames", 1)) except (TypeError, ValueError): frames_duration = None # Frame out, fallback on frame_in + duration or project's value or 1001 frame_out = item_data.pop("frame_out", None) if not frame_out: if frames_duration: - frame_out = frame_in + frames_duration + frame_out = frame_in + frames_duration - 1 else: frame_out = project_doc["data"].get("frameEnd", 1001) item_data["frameEnd"] = frame_out diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index b13c2dd4c6..b1a9b8b82c 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -174,7 +174,8 @@ def sync_zou_from_op_project( doc["name"], frame_in=doc["data"]["frameStart"], frame_out=doc["data"]["frameEnd"], - nb_frames=doc["data"]["frameEnd"] - doc["data"]["frameStart"], + nb_frames=( + doc["data"]["frameEnd"] - doc["data"]["frameStart"] + 1), ) elif match.group(2): # Sequence @@ -229,7 +230,7 @@ def sync_zou_from_op_project( "frame_in": frame_in, "frame_out": frame_out, }, - "nb_frames": frame_out - frame_in, + "nb_frames": frame_out - frame_in + 1, } ) entity = gazu.raw.update("entities", zou_id, entity_data) From c4f1a1f452d8e0f70d6a629db7925642d3acbc8d Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:13:51 +0100 Subject: [PATCH 082/160] Fixed hound's "line too long" comments --- openpype/modules/kitsu/actions/launcher_show_in_kitsu.py | 3 ++- openpype/modules/kitsu/utils/credentials.py | 3 ++- openpype/modules/kitsu/utils/sync_service.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index e3676afc4c..11224f6e52 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -67,7 +67,8 @@ class ShowInKitsu(LauncherAction): task_data = asset["data"]["tasks"][task_name] task_zou_data = task_data.get("zou", {}) if not task_zou_data: - self.log.debug("No zou task data for task: {}".format(task_name)) + self.log.debug( + "No zou task data for task: {}".format(task_name)) task_zou_id = task_zou_data["id"] # Define URL diff --git a/openpype/modules/kitsu/utils/credentials.py b/openpype/modules/kitsu/utils/credentials.py index 1731e1ca4f..941343cc8d 100644 --- a/openpype/modules/kitsu/utils/credentials.py +++ b/openpype/modules/kitsu/utils/credentials.py @@ -54,7 +54,8 @@ def validate_host(kitsu_url: str) -> bool: if gazu.client.host_is_valid(): return True else: - raise gazu.exception.HostException("Host '{}' is invalid.".format(kitsu_url)) + raise gazu.exception.HostException( + "Host '{}' is invalid.".format(kitsu_url)) def clear_credentials(): diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index eed259cda6..498c8de71e 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -494,7 +494,8 @@ class Listener: task_type_name = task['task_type']['name'] asset_tasks[task_type_name] = {"type": task_type_name, "zou": task} self.dbcon.update_one( - {"_id": asset_doc['_id']}, {"$set": {"data.tasks": asset_tasks}} + {"_id": asset_doc['_id']}, + {"$set": {"data.tasks": asset_tasks}} ) # Print message From 07ac3d8d4db54b111fd0117bed4a5cbc5c9b19d7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 15:58:49 +0100 Subject: [PATCH 083/160] If no task in instance, continue fix fore https://github.com/ynput/OpenPype/pull/4425#discussion_r1108582918 --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index aeec2481e0..54fb6a4678 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -22,10 +22,13 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Comment is `{}`".format(publish_comment)) for instance in context: + kitsu_task = instance.data.get("kitsu_task") + if kitsu_task is None: + continue # Get note status, by default uses the task status for the note # if it is not specified in the configuration - note_status = instance.data["kitsu_task"]["task_status"]["id"] + note_status = kitsu_task["task_status"]["id"] if self.set_status_note: kitsu_status = gazu.task.get_task_status_by_short_name( @@ -41,7 +44,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ) # Add comment to kitsu task - task = instance.data["kitsu_task"]["id"] + task = kitsu_task["id"] self.log.debug( "Add new note in taks id {}".format(task) ) From 915d11040493e1e59d2389e9d5f86f678ef4b9ca Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 16:02:53 +0100 Subject: [PATCH 084/160] Set frame_out to frame_in if no duration exists fix for https://github.com/ynput/OpenPype/pull/4425#discussion_r1108593566 --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 93d0d5e3fb..265c3638cd 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -124,7 +124,7 @@ def update_op_assets( if frames_duration: frame_out = frame_in + frames_duration - 1 else: - frame_out = project_doc["data"].get("frameEnd", 1001) + frame_out = project_doc["data"].get("frameEnd", frame_in) item_data["frameEnd"] = frame_out # Fps, fallback to project's value or default value (25.0) try: From 4bcef4406803f0210ed3a741ad99de8aa56c80a7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Sat, 18 Feb 2023 20:52:20 +0100 Subject: [PATCH 085/160] Fixed hound's max-length note --- openpype/modules/kitsu/utils/update_op_with_zou.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 265c3638cd..898cf076c8 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -146,7 +146,8 @@ def update_op_assets( "resolutionWidth") item_data["resolutionHeight"] = project_doc["data"].get( "resolutionHeight") - # Properties that doesn't fully exist in Kitsu. Guessing the property name + # Properties that doesn't fully exist in Kitsu. + # Guessing those property names below: # Pixel Aspect Ratio item_data["pixelAspect"] = item_data.get( "pixel_aspect", project_doc["data"].get("pixelAspect")) @@ -452,7 +453,8 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, project, project_doc, all_entities, zou_ids_and_asset_docs + dbcon, project, project_doc, + all_entities, zou_ids_and_asset_docs ) ] ) From 3fc2180e51357cc74a82a76f2fcd21fa11752ecc Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:06:52 +0100 Subject: [PATCH 086/160] Fixed all quotes types so they now match --- openpype/modules/kitsu/utils/sync_service.py | 133 ++++++++++--------- 1 file changed, 67 insertions(+), 66 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 498c8de71e..1af0b6edc4 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -48,16 +48,16 @@ class Listener: self.dbcon = AvalonMongoDB() self.dbcon.install() - gazu.client.set_host(os.environ['KITSU_SERVER']) + gazu.client.set_host(os.environ["KITSU_SERVER"]) # Authenticate if not validate_credentials(login, password): raise gazu.exception.AuthFailedException( - f"Kitsu authentication failed for login: '{login}'..." + 'Kitsu authentication failed for login: "{}"...'.format(login) ) gazu.set_event_host( - os.environ['KITSU_SERVER'].replace("api", "socket.io") + os.environ["KITSU_SERVER"].replace("api", "socket.io") ) self.event_client = gazu.events.init() @@ -135,14 +135,14 @@ class Listener: def _update_project(self, data): """Update project into OP DB.""" # Get project entity - project = gazu.project.get_project(data['project_id']) + project = gazu.project.get_project(data["project_id"]) update_project = write_project_to_op(project, self.dbcon) # Write into DB if update_project: - self.dbcon.Session['AVALON_PROJECT'] = get_kitsu_project_name( - data['project_id']) + self.dbcon.Session["AVALON_PROJECT"] = get_kitsu_project_name( + data["project_id"]) self.dbcon.bulk_write([update_project]) def _delete_project(self, data): @@ -168,10 +168,10 @@ class Listener: def _new_asset(self, data): """Create new asset into OP DB.""" # Get project entity - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) # Get asset entity - asset = gazu.asset.get_asset(data['asset_id']) + asset = gazu.asset.get_asset(data["asset_id"]) # Insert doc in DB self.dbcon.insert_one(create_op_asset(asset)) @@ -181,7 +181,7 @@ class Listener: # Print message episode = None - ep_id = asset['episode_id'] + ep_id = asset.get("episode_id") if ep_id and ep_id != "": episode = gazu.asset.get_episode(ep_id) @@ -195,22 +195,22 @@ class Listener: def _update_asset(self, data): """Update asset into OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - asset = gazu.asset.get_asset(data['asset_id']) + asset = gazu.asset.get_asset(data["asset_id"]) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc['data']['zou']['id']: asset_doc + asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in get_assets(project_name) - if asset_doc['data'].get("zou", {}).get("id") + if asset_doc["data"].get("zou", {}).get("id") } - zou_ids_and_asset_docs[asset['project_id']] = project_doc - gazu_project = gazu.project.get_project(asset['project_id']) + zou_ids_and_asset_docs[asset["project_id"]] = project_doc + gazu_project = gazu.project.get_project(asset["project_id"]) # Update update_op_result = update_op_assets( @@ -223,18 +223,18 @@ class Listener: def _delete_asset(self, data): """Delete asset of OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) - asset = self.dbcon.find_one({"data.zou.id": data['asset_id']}) + asset = self.dbcon.find_one({"data.zou.id": data["asset_id"]}) if asset: # Delete self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data['asset_id']} + {"type": "asset", "data.zou.id": data["asset_id"]} ) # Print message episode = None - ep_id = asset['data']['zou']['episode_id'] + ep_id = asset["data"]["zou"].get("episode_id") if ep_id and ep_id != "": episode = gazu.asset.get_episode(ep_id) @@ -250,10 +250,10 @@ class Listener: def _new_episode(self, data): """Create new episode into OP DB.""" # Get project entity - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) # Get gazu entity - episode = gazu.shot.get_episode(data['episode_id']) + ep = gazu.shot.get_episode(data["episode_id"]) # Insert doc in DB self.dbcon.insert_one(create_op_asset(episode)) @@ -268,22 +268,22 @@ class Listener: def _update_episode(self, data): """Update episode into OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - episode = gazu.shot.get_episode(data['episode_id']) + ep = gazu.shot.get_episode(data["episode_id"]) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc['data']['zou']['id']: asset_doc + asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in get_assets(project_name) - if asset_doc['data'].get("zou", {}).get("id") + if asset_doc["data"].get("zou", {}).get("id") } - zou_ids_and_asset_docs[episode['project_id']] = project_doc - gazu_project = gazu.project.get_project(episode['project_id']) + zou_ids_and_asset_docs[ep["project_id"]] = project_doc + gazu_project = gazu.project.get_project(ep["project_id"]) # Update update_op_result = update_op_assets( @@ -296,7 +296,7 @@ class Listener: def _delete_episode(self, data): """Delete shot of OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) episode = self.dbcon.find_one({"data.zou.id": data['episode_id']}) if episode: @@ -317,10 +317,10 @@ class Listener: def _new_sequence(self, data): """Create new sequnce into OP DB.""" # Get project entity - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) # Get gazu entity - sequence = gazu.shot.get_sequence(data['sequence_id']) + sequence = gazu.shot.get_sequence(data["sequence_id"]) # Insert doc in DB self.dbcon.insert_one(create_op_asset(sequence)) @@ -331,7 +331,7 @@ class Listener: # Print message episode = None - ep_id = sequence['episode_id'] + ep_id = sequence.get("episode_id") if ep_id and ep_id != "": episode = gazu.asset.get_episode(ep_id) @@ -344,22 +344,22 @@ class Listener: def _update_sequence(self, data): """Update sequence into OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - sequence = gazu.shot.get_sequence(data['sequence_id']) + sequence = gazu.shot.get_sequence(data["sequence_id"]) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc['data']['zou']['id']: asset_doc + asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in get_assets(project_name) - if asset_doc['data'].get("zou", {}).get("id") + if asset_doc["data"].get("zou", {}).get("id") } - zou_ids_and_asset_docs[sequence['project_id']] = project_doc - gazu_project = gazu.project.get_project(sequence['project_id']) + zou_ids_and_asset_docs[sequence["project_id"]] = project_doc + gazu_project = gazu.project.get_project(sequence["project_id"]) # Update update_op_result = update_op_assets( @@ -372,15 +372,16 @@ class Listener: def _delete_sequence(self, data): """Delete sequence of OP DB.""" - set_op_project(self.dbcon, data['project_id']) - sequence = self.dbcon.find_one({"data.zou.id": data['sequence_id']}) + set_op_project(self.dbcon, data["project_id"]) + sequence = self.dbcon.find_one({"data.zou.id": data["sequence_id"]}) if sequence: # Delete self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data['sequence_id']} + {"type": "asset", "data.zou.id": data["sequence_id"]} ) # Print message + ep_id = sequence["data"]["zou"].get("episode_id") gazu_project = gazu.project.get_project( sequence['data']['zou']['project_id']) msg = f"Sequence deleted: " @@ -392,10 +393,10 @@ class Listener: def _new_shot(self, data): """Create new shot into OP DB.""" # Get project entity - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) # Get gazu entity - shot = gazu.shot.get_shot(data['shot_id']) + shot = gazu.shot.get_shot(data["shot_id"]) # Insert doc in DB self.dbcon.insert_one(create_op_asset(shot)) @@ -405,7 +406,7 @@ class Listener: # Print message episode = None - if shot['episode_id'] and shot['episode_id'] != "": + if shot["episode_id"] and shot["episode_id"] != "": episode = gazu.asset.get_episode(shot['episode_id']) msg = "Shot created: " @@ -418,21 +419,21 @@ class Listener: def _update_shot(self, data): """Update shot into OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) project_name = self.dbcon.active_project() project_doc = get_project(project_name) # Get gazu entity - shot = gazu.shot.get_shot(data['shot_id']) + shot = gazu.shot.get_shot(data["shot_id"]) # Find asset doc # Query all assets of the local project zou_ids_and_asset_docs = { - asset_doc['data']['zou']['id']: asset_doc + asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in get_assets(project_name) - if asset_doc['data'].get("zou", {}).get("id") - } - zou_ids_and_asset_docs[shot['project_id']] = project_doc + if asset_doc["data"].get("zou", {}).get("id")} + zou_ids_and_asset_docs[shot["project_id"]] = project_doc + gazu_project = gazu.project.get_project(shot["project_id"]) gazu_project = gazu.project.get_project(shot['project_id']) # Update @@ -447,18 +448,18 @@ class Listener: def _delete_shot(self, data): """Delete shot of OP DB.""" - set_op_project(self.dbcon, data['project_id']) - shot = self.dbcon.find_one({"data.zou.id": data['shot_id']}) + set_op_project(self.dbcon, data["project_id"]) + shot = self.dbcon.find_one({"data.zou.id": data["shot_id"]}) if shot: # Delete self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data['shot_id']} + {"type": "asset", "data.zou.id": data["shot_id"]} ) # Print message gazu_project = gazu.project.get_project( - shot['data']['zou']['project_id']) + ep_id = shot["data"]["zou"].get("episode_id") msg = "Shot deleted: " msg = msg + f"{gazu_project['name']} - " @@ -469,15 +470,15 @@ class Listener: def _new_task(self, data): """Create new task into OP DB.""" # Get project entity - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) project_name = self.dbcon.active_project() # Get gazu entity - task = gazu.task.get_task(data['task_id']) + task = gazu.task.get_task(data["task_id"]) # Find asset doc episode = None - ep_id = task.get('episode_id') + ep_id = task.get("episode_id") if ep_id and ep_id != "": episode = gazu.asset.get_episode(ep_id) @@ -490,11 +491,11 @@ class Listener: # Update asset tasks with new one asset_doc = get_asset_by_name(project_name, parent_name) if asset_doc: - asset_tasks = asset_doc['data'].get("tasks") - task_type_name = task['task_type']['name'] + asset_tasks = asset_doc["data"].get("tasks") + task_type_name = task["task_type"]["name"] asset_tasks[task_type_name] = {"type": task_type_name, "zou": task} self.dbcon.update_one( - {"_id": asset_doc['_id']}, + {"_id": asset_doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} ) @@ -515,29 +516,29 @@ class Listener: def _delete_task(self, data): """Delete task of OP DB.""" - set_op_project(self.dbcon, data['project_id']) + set_op_project(self.dbcon, data["project_id"]) project_name = self.dbcon.active_project() # Find asset doc asset_docs = list(get_assets(project_name)) for doc in asset_docs: # Match task - for name, task in doc['data']['tasks'].items(): - if task.get("zou") and data['task_id'] == task['zou']['id']: + for name, task in doc["data"]["tasks"].items(): + if task.get("zou") and data["task_id"] == task["zou"]["id"]: # Pop task - asset_tasks = doc['data'].get("tasks", {}) + asset_tasks = doc["data"].get("tasks", {}) asset_tasks.pop(name) # Delete task in DB self.dbcon.update_one( - {"_id": doc['_id']}, + {"_id": doc["_id"]}, {"$set": {"data.tasks": asset_tasks}}, ) # Print message - shot = gazu.shot.get_shot(task['zou']['entity_id']) + entity = gazu.entity.get_entity(task["zou"]["entity_id"]) episode = None - ep_id = shot['episode_id'] + ep_id = entity.get("episode_id") if ep_id and ep_id != "": episode = gazu.asset.get_episode(ep_id) From f9137bdb041690f2ef6cc0a3d26878ee70b321bf Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:08:40 +0100 Subject: [PATCH 087/160] Added docstring and changed `doc` to `dict` in var-names --- .../modules/kitsu/utils/update_op_with_zou.py | 78 +++++++++++-------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 898cf076c8..9368848532 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -65,30 +65,32 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( dbcon: AvalonMongoDB, gazu_project: dict, - project_doc: dict, + project_dict: dict, entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + asset_dict_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. Args: dbcon (AvalonMongoDB): Connection to DB + gazu_project dict): Dict of gazu, + project_dict dict): Dict of project, entities_list (List[dict]): List of zou entities to update - asset_doc_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...] + asset_dict_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...] Returns: List[Dict[str, dict]]: List of (doc_id, update_dict) tuples """ - if not project_doc: + if not project_dict: return - project_name = project_doc["name"] + project_name = project_dict["name"] assets_with_update = [] for item in entities_list: # Check asset exists - item_doc = asset_doc_ids.get(item["id"]) + item_doc = asset_dict_ids.get(item["id"]) if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) @@ -105,7 +107,7 @@ def update_op_assets( try: frame_in = int( item_data.pop( - "frame_in", project_doc["data"].get("frameStart") + "frame_in", project_dict["data"].get("frameStart") ) ) except (TypeError, ValueError): @@ -124,14 +126,14 @@ def update_op_assets( if frames_duration: frame_out = frame_in + frames_duration - 1 else: - frame_out = project_doc["data"].get("frameEnd", frame_in) + frame_out = project_dict["data"].get("frameEnd", frame_in) item_data["frameEnd"] = frame_out # Fps, fallback to project's value or default value (25.0) try: fps = float(item_data.get("fps")) except (TypeError, ValueError): fps = float(gazu_project.get( - "fps", project_doc["data"].get("fps", 25))) + "fps", project_dict["data"].get("fps", 25))) item_data["fps"] = fps # Resolution, fall back to project default match_res = re.match( @@ -142,27 +144,27 @@ def update_op_assets( item_data["resolutionWidth"] = int(match_res.group(1)) item_data["resolutionHeight"] = int(match_res.group(2)) else: - item_data["resolutionWidth"] = project_doc["data"].get( + item_data["resolutionWidth"] = project_dict["data"].get( "resolutionWidth") - item_data["resolutionHeight"] = project_doc["data"].get( + item_data["resolutionHeight"] = project_dict["data"].get( "resolutionHeight") # Properties that doesn't fully exist in Kitsu. # Guessing those property names below: # Pixel Aspect Ratio item_data["pixelAspect"] = item_data.get( - "pixel_aspect", project_doc["data"].get("pixelAspect")) + "pixel_aspect", project_dict["data"].get("pixelAspect")) # Handle Start item_data["handleStart"] = item_data.get( - "handle_start", project_doc["data"].get("handleStart")) + "handle_start", project_dict["data"].get("handleStart")) # Handle End item_data["handleEnd"] = item_data.get( - "handle_end", project_doc["data"].get("handleEnd")) + "handle_end", project_dict["data"].get("handleEnd")) # Clip In item_data["clipIn"] = item_data.get( - "clip_in", project_doc["data"].get("clipIn")) + "clip_in", project_dict["data"].get("clipIn")) # Clip Out item_data["clipOut"] = item_data.get( - "clip_out", project_doc["data"].get("clipOut")) + "clip_out", project_dict["data"].get("clipOut")) # Tasks tasks_list = [] @@ -204,9 +206,14 @@ def update_op_assets( entity_root_asset_name = "Shots" # Root parent folder if exist - visual_parent_doc_id = ( - asset_doc_ids[parent_zou_id].get("_id") if parent_zou_id else None - ) + visual_parent_doc_id = None + if parent_zou_id is not None: + parent_zou_id_dict = asset_dict_ids.get(parent_zou_id) + if parent_zou_id_dict is not None: + visual_parent_doc_id = ( + parent_zou_id_dict.get("_id") + if parent_zou_id_dict else None) + if visual_parent_doc_id is None: # Find root folder doc ("Assets" or "Shots") root_folder_doc = get_asset_by_name( @@ -225,12 +232,15 @@ def update_op_assets( item_data["parents"] = [] ancestor_id = parent_zou_id while ancestor_id is not None: - parent_doc = asset_doc_ids[ancestor_id] - item_data["parents"].insert(0, parent_doc["name"]) + parent_doc = asset_dict_ids.get(ancestor_id) + if parent_doc is not None: + item_data["parents"].insert(0, parent_doc["name"]) - # Get parent entity - parent_entity = parent_doc["data"]["zou"] - ancestor_id = parent_entity.get("parent_id") + # Get parent entity + parent_entity = parent_doc["data"]["zou"] + ancestor_id = parent_entity.get("parent_id") + else: + ancestor_id = None # Build OpenPype compatible name if item_type in ["Shot", "Sequence"] and parent_zou_id is not None: @@ -239,7 +249,7 @@ def update_op_assets( item_name = f"{item_data['parents'][-1]}_{item['name']}" # Update doc name - asset_doc_ids[item["id"]]["name"] = item_name + asset_dict_ids[item["id"]]["name"] = item_name else: item_name = item["name"] @@ -258,7 +268,7 @@ def update_op_assets( "$set": { "name": item_name, "data": item_data, - "parent": project_doc["_id"], + "parent": project_dict["_id"], } }, ) @@ -278,13 +288,13 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: UpdateOne: Update instance for the project """ project_name = project["name"] - project_doc = get_project(project_name) - if not project_doc: + project_dict = get_project(project_name) + if not project_dict: log.info("Project created: {}".format(project_name)) - project_doc = create_project(project_name, project_name) + project_dict = create_project(project_name, project_name) # Project data and tasks - project_data = project_doc["data"] or {} + project_data = project_dict["data"] or {} # Build project code and update Kitsu project_code = project.get("code") @@ -315,7 +325,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) return UpdateOne( - {"_id": project_doc["_id"]}, + {"_id": project_dict["_id"]}, { "$set": { "config.tasks": { @@ -398,7 +408,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Try to find project document project_name = project["name"] dbcon.Session["AVALON_PROJECT"] = project_name - project_doc = get_project(project_name) + project_dict = get_project(project_name) # Query all assets of the local project zou_ids_and_asset_docs = { @@ -406,7 +416,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): for asset_doc in get_assets(project_name) if asset_doc["data"].get("zou", {}).get("id") } - zou_ids_and_asset_docs[project["id"]] = project_doc + zou_ids_and_asset_docs[project["id"]] = project_dict # Create entities root folders to_insert = [ @@ -453,7 +463,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, project, project_doc, + dbcon, project, project_dict, all_entities, zou_ids_and_asset_docs ) ] From 0f76d3a44e4974b5c0ec81f166a19289d9cb4fd6 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:13:33 +0100 Subject: [PATCH 088/160] Cleaned up the fetching of the entity_id Also changed the name kitsu_id to entity_id and kitsu_entity to just entity as that's what it is. --- .../plugins/publish/collect_kitsu_entities.py | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index dc7048cf2a..1531c80e04 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -16,7 +16,6 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_entities_by_id = {} for instance in context: asset_doc = instance.data.get("assetEntity") - task_name = instance.data.get("task") if not asset_doc: continue @@ -24,27 +23,24 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): if not zou_asset_data: raise ValueError("Zou asset data not found in OpenPype!") - if kitsu_project is None: - kitsu_project = gazu.project.get_project( - zou_asset_data["project_id"]) - if not kitsu_project: - raise ValueError("Project not found in kitsu!") + kitsu_project = gazu.project.get_project( + zou_asset_data["project_id"]) + if not kitsu_project: + raise ValueError("Project not found in kitsu!") - entity_type = zou_asset_data["type"] - kitsu_id = zou_asset_data["id"] - kitsu_entity = kitsu_entities_by_id.get(kitsu_id) - if not kitsu_entity: - if entity_type == "Shot": - kitsu_entity = gazu.shot.get_shot(kitsu_id) - else: - kitsu_entity = gazu.asset.get_asset(kitsu_id) - kitsu_entities_by_id[kitsu_id] = kitsu_entity + entity_id = zou_asset_data["id"] + entity = kitsu_entities_by_id.get(entity_id) + if not entity: + entity = gazu.entity.get_entity(entity_id) + if not entity: + raise ValueError( + "{} was not found in kitsu!".format( + zou_asset_data["name"])) - if not kitsu_entity: - raise ValueError( - "{} not found in kitsu!".format(entity_type)) - instance.data["kitsu_entity"] = kitsu_entity + kitsu_entities_by_id[entity_id] = entity + instance.data["entity"] = entity + task_name = instance.data.get("task") if not task_name: continue zou_task_data = asset_doc["data"]["tasks"][task_name].get("zou") From 13a4c7201e5fd410e3546b4cd898ab8f0068b4ab Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:18:43 +0100 Subject: [PATCH 089/160] change task to task_id --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 54fb6a4678..006f0bc6d0 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -44,12 +44,12 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ) # Add comment to kitsu task - task = kitsu_task["id"] + task_id = kitsu_task["id"] self.log.debug( - "Add new note in taks id {}".format(task) + "Add new note in taks id {}".format(task_id) ) kitsu_comment = gazu.task.add_comment( - task, note_status, comment=publish_comment + task_id, note_status, comment=publish_comment ) instance.data["kitsu_comment"] = kitsu_comment From bcea2c70a269559a2fa370185393d6c4b675a38b Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:20:32 +0100 Subject: [PATCH 090/160] Cleaned up project deletion code --- openpype/modules/kitsu/utils/sync_service.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 1af0b6edc4..6155b396aa 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -149,20 +149,16 @@ class Listener: """Delete project.""" collections = self.dbcon.database.list_collection_names() - project_name = None for collection in collections: - post = self.dbcon.database[collection].find_one( - {"data.zou_id": data['project_id']}) - if post: - project_name = post['name'] - break + project = self.dbcon.database[collection].find_one( + {"data.zou_id": data["project_id"]}) + if project: + # Delete project collection + self.dbcon.database[project["name"]].drop() - if project_name: - # Delete project collection - self.dbcon.database[project_name].drop() - - # Print message - log.info("Project deleted: {}".format(project_name)) + # Print message + log.info("Project deleted: {}".format(project["name"])) + return # == Asset == def _new_asset(self, data): From d9ac1ee95255c2d663cffaa00e63ad99a74188af Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:24:55 +0100 Subject: [PATCH 091/160] Cleaned up log.info() message creation --- openpype/modules/kitsu/utils/sync_service.py | 171 +++++++++++-------- 1 file changed, 96 insertions(+), 75 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 6155b396aa..b389d25c4f 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -176,17 +176,18 @@ class Listener: self._update_asset(data) # Print message - episode = None + ep = None ep_id = asset.get("episode_id") if ep_id and ep_id != "": - episode = gazu.asset.get_episode(ep_id) + ep = gazu.asset.get_episode(ep_id) - msg = "Asset created: " - msg = msg + f"{asset['project_name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{asset['asset_type_name']}_" - msg = msg + f"{asset['name']}" + msg = "Asset created: {proj_name} - {ep_name}" \ + "{asset_type_name} - {asset_name}".format( + proj_name=asset["project_name"], + ep_name=ep["name"] + " - " if ep is not None else "", + asset_type_name=asset["asset_type_name"], + asset_name=asset["name"] + ) log.info(msg) def _update_asset(self, data): @@ -229,17 +230,18 @@ class Listener: ) # Print message - episode = None + ep = None ep_id = asset["data"]["zou"].get("episode_id") if ep_id and ep_id != "": - episode = gazu.asset.get_episode(ep_id) + ep = gazu.asset.get_episode(ep_id) - msg = "Asset deleted: " - msg = msg + f"{asset['data']['zou']['project_name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{asset['data']['zou']['asset_type_name']}_" - msg = msg + f"'{asset['name']}" + msg = "Asset deleted: {proj_name} - {ep_name}" \ + "{asset_type_name} - {asset_name}".format( + proj_name=asset["data"]["zou"]["project_name"], + ep_name=ep["name"] + " - " if ep is not None else "", + asset_type_name=asset["data"]["zou"]["asset_type_name"], + asset_name=asset["name"] + ) log.info(msg) # == Episode == @@ -252,15 +254,17 @@ class Listener: ep = gazu.shot.get_episode(data["episode_id"]) # Insert doc in DB - self.dbcon.insert_one(create_op_asset(episode)) + self.dbcon.insert_one(create_op_asset(ep)) # Update self._update_episode(data) # Print message - msg = "Episode created: " - msg = msg + f"{episode['project_name']} - " - msg = msg + f"{episode['name']}" + msg = "Episode created: {proj_name} - {ep_name}".format( + proj_name=ep["project_name"], + ep_name=ep["name"] + ) + log.info(msg) def _update_episode(self, data): """Update episode into OP DB.""" @@ -283,8 +287,8 @@ class Listener: # Update update_op_result = update_op_assets( - self.dbcon, gazu_project, project_doc, [ - episode], zou_ids_and_asset_docs + self.dbcon, gazu_project, project_doc, + [ep], zou_ids_and_asset_docs ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -294,20 +298,22 @@ class Listener: """Delete shot of OP DB.""" set_op_project(self.dbcon, data["project_id"]) - episode = self.dbcon.find_one({"data.zou.id": data['episode_id']}) - if episode: + ep = self.dbcon.find_one({"data.zou.id": data["episode_id"]}) + if ep: # Delete self.dbcon.delete_one( - {"type": "asset", "data.zou.id": data['episode_id']} + {"type": "asset", "data.zou.id": data["episode_id"]} ) # Print message project = gazu.project.get_project( - episode['data']['zou']['project_id']) + ep["data"]["zou"]["project_id"]) - msg = "Episode deleted: " - msg = msg + f"{project['name']} - " - msg = msg + f"{episode['name']}" + msg = "Episode deleted: {proj_name} - {ep_name}".format( + proj_name=project["name"], + ep_name=ep["name"] + ) + log.info(msg) # == Sequence == def _new_sequence(self, data): @@ -325,17 +331,17 @@ class Listener: self._update_sequence(data) # Print message - - episode = None + ep = None ep_id = sequence.get("episode_id") if ep_id and ep_id != "": - episode = gazu.asset.get_episode(ep_id) + ep = gazu.asset.get_episode(ep_id) - msg = "Sequence created: " - msg = msg + f"{sequence['project_name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{sequence['name']}" + msg = "Sequence created: {proj_name} - {ep_name}" \ + "{sequence_name}".format( + proj_name=sequence["project_name"], + ep_name=ep["name"] + " - " if ep is not None else "", + sequence_name=sequence["name"] + ) log.info(msg) def _update_sequence(self, data): @@ -377,12 +383,20 @@ class Listener: ) # Print message + ep = None ep_id = sequence["data"]["zou"].get("episode_id") + if ep_id and ep_id != "": + ep = gazu.asset.get_episode(ep_id) + gazu_project = gazu.project.get_project( - sequence['data']['zou']['project_id']) - msg = f"Sequence deleted: " - msg = msg + f"{gazu_project['name']} - " - msg = msg + f"{sequence['name']}" + sequence["data"]["zou"]["project_id"]) + + msg = "Sequence created: {proj_name} - {ep_name}" \ + "{sequence_name}".format( + proj_name=gazu_project["name"], + ep_name=ep["name"] + " - " if ep is not None else "", + sequence_name=sequence["name"] + ) log.info(msg) # == Shot == @@ -401,16 +415,17 @@ class Listener: self._update_shot(data) # Print message - episode = None + ep = None if shot["episode_id"] and shot["episode_id"] != "": - episode = gazu.asset.get_episode(shot['episode_id']) + ep = gazu.asset.get_episode(shot["episode_id"]) - msg = "Shot created: " - msg = msg + f"{shot['project_name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{shot['sequence_name']}_" - msg = msg + f"{shot['name']}" + msg = "Shot created: {proj_name} - {ep_name}" \ + "{sequence_name} - {shot_name}".format( + proj_name=shot["project_name"], + ep_name=ep["name"] + " - " if ep is not None else "", + sequence_name=shot["sequence_name"], + shot_name=shot["name"] + ) log.info(msg) def _update_shot(self, data): @@ -430,7 +445,6 @@ class Listener: if asset_doc["data"].get("zou", {}).get("id")} zou_ids_and_asset_docs[shot["project_id"]] = project_doc gazu_project = gazu.project.get_project(shot["project_id"]) - gazu_project = gazu.project.get_project(shot['project_id']) # Update update_op_result = update_op_assets( @@ -454,12 +468,18 @@ class Listener: ) # Print message - gazu_project = gazu.project.get_project( + ep = None ep_id = shot["data"]["zou"].get("episode_id") + if ep_id and ep_id != "": + ep = gazu.asset.get_episode(ep_id) - msg = "Shot deleted: " - msg = msg + f"{gazu_project['name']} - " - msg = msg + f"{shot['name']}" + msg = "Shot deleted: {proj_name} - {ep_name}" \ + "{sequence_name} - {shot_name}".format( + proj_name=shot["data"]["zou"]["project_name"], + ep_name=ep["name"] + " - " if ep is not None else "", + sequence_name=shot["data"]["zou"]["sequence_name"], + shot_name=shot["name"] + ) log.info(msg) # == Task == @@ -472,14 +492,14 @@ class Listener: # Get gazu entity task = gazu.task.get_task(data["task_id"]) - # Find asset doc - episode = None + # Print message + ep = None ep_id = task.get("episode_id") if ep_id and ep_id != "": - episode = gazu.asset.get_episode(ep_id) + ep = gazu.asset.get_episode(ep_id) - parent_name = "" - if episode is not None: + parent_name = None + entity_type = None parent_name = episode['name'] + "_" parent_name = parent_name + \ task['sequence']['name'] + "_" + task['entity']['name'] @@ -496,13 +516,13 @@ class Listener: ) # Print message - msg = "Task created: " - msg = msg + f"{task['project']['name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{task['sequence']['name']}_" - msg = msg + f"{task['entity']['name']} - " - msg = msg + f"{task['task_type']['name']}" + msg = "Task created: {proj_name} - {entity_type}{parent_name}" \ + " - {task_name}".format( + proj_name=task["project"]["name"], + entity_type=entity_type + " - " if entity_type is not None else "", + parent_name=parent_name, + task_name=task["task_type"]["name"] + ) log.info(msg) def _update_task(self, data): @@ -533,19 +553,20 @@ class Listener: # Print message entity = gazu.entity.get_entity(task["zou"]["entity_id"]) - episode = None + ep = None ep_id = entity.get("episode_id") if ep_id and ep_id != "": - episode = gazu.asset.get_episode(ep_id) + ep = gazu.asset.get_episode(ep_id) - msg = "Task deleted: " - msg = msg + f"{shot['project_name']} - " - if episode is not None: - msg = msg + f"{episode['name']}_" - msg = msg + f"{shot['sequence_name']}_" - msg = msg + f"{shot['name']} - " - msg = msg + f"{task['type']}" + msg = "Task deleted: {proj_name} - {entity_type}{parent_name}" \ + " - {task_name}".format( + proj_name=task["zou"]["project"]["name"], + entity_type=entity_type + " - " if entity_type is not None else "", + parent_name=parent_name, + task_name=task["type"] + ) log.info(msg) + return From d153e6c224a6d2ea21b11a1c33e88b2c19049123 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 28 Feb 2023 23:26:10 +0100 Subject: [PATCH 092/160] Split up the difference in name and type Assets/Shots generate Before it was only working for shots. Now it also works for Assets. I'm ding an elif as Kitsu now also have tasks for sequences, edits and other things. Will try and add those in at a later stage. --- openpype/modules/kitsu/utils/sync_service.py | 24 +++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index b389d25c4f..7e7f3f557c 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -500,9 +500,15 @@ class Listener: parent_name = None entity_type = None - parent_name = episode['name'] + "_" - parent_name = parent_name + \ - task['sequence']['name'] + "_" + task['entity']['name'] + if task["task_type"]["for_entity"] == "Asset": + parent_name = task["entity"]["name"] + entity_type = task["entity_type"]["name"] + elif task["task_type"]["for_entity"] == "Shot": + parent_name = "{ep_name}{sequence_name} - {shot_name}".format( + ep_name=ep["name"] + " - " if ep is not None else "", + sequence_name=task["sequence"]["name"], + shot_name=task["entity"]["name"] + ) # Update asset tasks with new one asset_doc = get_asset_by_name(project_name, parent_name) @@ -558,6 +564,18 @@ class Listener: if ep_id and ep_id != "": ep = gazu.asset.get_episode(ep_id) + parent_name = None + entity_type = None + if task["task_type"]["for_entity"] == "Asset": + parent_name = task["entity"]["name"] + entity_type = task["entity_type"]["name"] + elif task["task_type"]["for_entity"] == "Shot": + parent_name = "{ep_name}{sequence_name} - {shot_name}".format( + ep_name=ep["name"] + " - " if ep is not None else "", + sequence_name=task["sequence"]["name"], + shot_name=task["entity"]["name"] + ) + msg = "Task deleted: {proj_name} - {entity_type}{parent_name}" \ " - {task_name}".format( proj_name=task["zou"]["project"]["name"], From 7965b91dafd59e0a75b498961d94aa8fdaa14467 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Mar 2023 11:02:25 +0100 Subject: [PATCH 093/160] Moved kitsu_project out of context loop --- .../plugins/publish/collect_kitsu_entities.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 1531c80e04..f68226a4a5 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -11,7 +11,13 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): def process(self, context): - kitsu_project = None + kitsu_project = gazu.project.get_project_by_name( + context.data["projectName"]) + if not kitsu_project: + raise ValueError("Project not found in kitsu!") + + context.data["kitsu_project"] = kitsu_project + self.log.debug("Collect kitsu project: {}".format(kitsu_project)) kitsu_entities_by_id = {} for instance in context: @@ -23,10 +29,10 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): if not zou_asset_data: raise ValueError("Zou asset data not found in OpenPype!") - kitsu_project = gazu.project.get_project( - zou_asset_data["project_id"]) - if not kitsu_project: - raise ValueError("Project not found in kitsu!") + task_name = instance.data.get("task") + if not task_name: + continue + entity_id = zou_asset_data["id"] entity = kitsu_entities_by_id.get(entity_id) @@ -63,5 +69,3 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): raise ValueError("Task not found in kitsu!") instance.data["kitsu_task"] = kitsu_task self.log.debug("Collect kitsu task: {}".format(kitsu_task)) - - context.data["kitsu_project"] = kitsu_project From 962d0783b06d804cacf1ce628d17e1d0836951b2 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Mar 2023 11:05:38 +0100 Subject: [PATCH 094/160] Fixed fetching of kitsu_task + moved data checks to the top of loop --- .../plugins/publish/collect_kitsu_entities.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index f68226a4a5..9b34bd15a9 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -33,6 +33,9 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): if not task_name: continue + zou_task_data = asset_doc["data"]["tasks"][task_name].get("zou") + self.log.debug( + "Collected zou task data: {}".format(zou_task_data)) entity_id = zou_asset_data["id"] entity = kitsu_entities_by_id.get(entity_id) @@ -45,25 +48,26 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_entities_by_id[entity_id] = entity instance.data["entity"] = entity - - task_name = instance.data.get("task") - if not task_name: - continue - zou_task_data = asset_doc["data"]["tasks"][task_name].get("zou") self.log.debug( - "Collected zou task data: {}".format(zou_task_data)) - if not zou_task_data: + "Collect kitsu {}: {}".format(zou_asset_data["type"], entity) + ) + + if zou_task_data: + kitsu_task_id = zou_task_data["id"] + kitsu_task = kitsu_entities_by_id.get(kitsu_task_id) + if not kitsu_task: + kitsu_task = gazu.task.get_task(zou_task_data["id"]) + kitsu_entities_by_id[kitsu_task_id] = kitsu_task + else: kitsu_task_type = gazu.task.get_task_type_by_name(task_name) if not kitsu_task_type: raise ValueError( "Task type {} not found in Kitsu!".format(task_name) ) - continue - kitsu_task_id = zou_task_data["id"] - kitsu_task = kitsu_entities_by_id.get(kitsu_task_id) - if not kitsu_task: - kitsu_task = gazu.task.get_task(zou_task_data["id"]) - kitsu_entities_by_id[kitsu_task_id] = kitsu_task + + kitsu_task = gazu.task.get_task_by_name( + entity, kitsu_task_type + ) if not kitsu_task: raise ValueError("Task not found in kitsu!") From 8bf970e8b96b80c2cae601530d076b27d3a6d8f7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Mar 2023 11:16:05 +0100 Subject: [PATCH 095/160] fixed hound's comments --- .../kitsu/plugins/publish/collect_kitsu_entities.py | 2 +- openpype/modules/kitsu/utils/sync_service.py | 9 ++++++--- openpype/modules/kitsu/utils/update_op_with_zou.py | 6 ++++-- openpype/modules/kitsu/utils/update_zou_with_op.py | 3 ++- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 9b34bd15a9..71ed563580 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -64,7 +64,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): raise ValueError( "Task type {} not found in Kitsu!".format(task_name) ) - + kitsu_task = gazu.task.get_task_by_name( entity, kitsu_task_type ) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 7e7f3f557c..9c5c9e24ec 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -101,7 +101,8 @@ class Listener: self.event_client, "sequence:delete", self._delete_sequence ) - gazu.events.add_listener(self.event_client, "shot:new", self._new_shot) + gazu.events.add_listener( + self.event_client, "shot:new", self._new_shot) gazu.events.add_listener( self.event_client, "shot:update", self._update_shot ) @@ -109,7 +110,8 @@ class Listener: self.event_client, "shot:delete", self._delete_shot ) - gazu.events.add_listener(self.event_client, "task:new", self._new_task) + gazu.events.add_listener( + self.event_client, "task:new", self._new_task) gazu.events.add_listener( self.event_client, "task:update", self._update_task ) @@ -515,7 +517,8 @@ class Listener: if asset_doc: asset_tasks = asset_doc["data"].get("tasks") task_type_name = task["task_type"]["name"] - asset_tasks[task_type_name] = {"type": task_type_name, "zou": task} + asset_tasks[task_type_name] = { + "type": task_type_name, "zou": task} self.dbcon.update_one( {"_id": asset_doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 9368848532..6590d05a82 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -94,7 +94,8 @@ def update_op_assets( if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) - item_doc = get_asset_by_id(project_name, insert_result.inserted_id) + item_doc = get_asset_by_id( + project_name, insert_result.inserted_id) # Update asset item_data = deepcopy(item_doc["data"]) @@ -339,7 +340,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects(login: str, password: str, ignore_projects: list = None): +def sync_all_projects( + login: str, password: str, ignore_projects: list = None): """Update all OP projects in DB with Zou data. Args: diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index b1a9b8b82c..617f037c1e 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -82,7 +82,8 @@ def sync_zou_from_op_project( f"x{project_doc['data']['resolutionHeight']}", } ) - gazu.project.update_project_data(zou_project, data=project_doc["data"]) + gazu.project.update_project_data( + zou_project, data=project_doc["data"]) gazu.project.update_project(zou_project) asset_types = gazu.asset.all_asset_types() From 0d981a61291d78f25c53425de4161d3f109f1505 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Mar 2023 11:42:18 +0100 Subject: [PATCH 096/160] Fixed hound's comments --- openpype/modules/kitsu/utils/sync_service.py | 41 ++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 9c5c9e24ec..da81a23495 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -501,10 +501,10 @@ class Listener: ep = gazu.asset.get_episode(ep_id) parent_name = None - entity_type = None + ent_type = None if task["task_type"]["for_entity"] == "Asset": parent_name = task["entity"]["name"] - entity_type = task["entity_type"]["name"] + ent_type = task["entity_type"]["name"] elif task["task_type"]["for_entity"] == "Shot": parent_name = "{ep_name}{sequence_name} - {shot_name}".format( ep_name=ep["name"] + " - " if ep is not None else "", @@ -525,12 +525,12 @@ class Listener: ) # Print message - msg = "Task created: {proj_name} - {entity_type}{parent_name}" \ - " - {task_name}".format( - proj_name=task["project"]["name"], - entity_type=entity_type + " - " if entity_type is not None else "", - parent_name=parent_name, - task_name=task["task_type"]["name"] + msg = "Task created: {proj} - {ent_type}{parent}" \ + " - {task}".format( + proj=task["project"]["name"], + ent_type=ent_type + " - " if ent_type is not None else "", + parent=parent_name, + task=task["task_type"]["name"] ) log.info(msg) @@ -568,23 +568,24 @@ class Listener: ep = gazu.asset.get_episode(ep_id) parent_name = None - entity_type = None + ent_type = None if task["task_type"]["for_entity"] == "Asset": parent_name = task["entity"]["name"] - entity_type = task["entity_type"]["name"] + ent_type = task["entity_type"]["name"] elif task["task_type"]["for_entity"] == "Shot": - parent_name = "{ep_name}{sequence_name} - {shot_name}".format( - ep_name=ep["name"] + " - " if ep is not None else "", - sequence_name=task["sequence"]["name"], - shot_name=task["entity"]["name"] + parent_name = "{ep}{sequence} - {shot}".format( + ep=ep["name"] + " - " if ep is not None else "", + sequence=task["sequence"]["name"], + shot=task["entity"]["name"] ) - msg = "Task deleted: {proj_name} - {entity_type}{parent_name}" \ - " - {task_name}".format( - proj_name=task["zou"]["project"]["name"], - entity_type=entity_type + " - " if entity_type is not None else "", - parent_name=parent_name, - task_name=task["type"] + ent_type=ent_type + " - " if ent_type is not None else "", + msg = "Task deleted: {proj} - {ent_type}{parent}" \ + " - {task}".format( + proj=task["zou"]["project"]["name"], + ent_type=ent_type, + parent=parent_name, + task=task["type"] ) log.info(msg) From 0beec8c3a710e7c3e950678a4c0b6606f4f079d7 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Mar 2023 11:44:55 +0100 Subject: [PATCH 097/160] Fixed hound's comments --- openpype/modules/kitsu/utils/sync_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index da81a23495..91ce84637d 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -579,7 +579,7 @@ class Listener: shot=task["entity"]["name"] ) - ent_type=ent_type + " - " if ent_type is not None else "", + ent_type=ent_type + " - " if ent_type is not None else "" msg = "Task deleted: {proj} - {ent_type}{parent}" \ " - {task}".format( proj=task["zou"]["project"]["name"], From 247778575f2e5c03cf055da288b444034bca4475 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 12:30:19 +0100 Subject: [PATCH 098/160] Project creation logs happens outside of write_project_to_op() function --- openpype/modules/kitsu/utils/sync_service.py | 12 ++++++------ openpype/modules/kitsu/utils/update_op_with_zou.py | 9 ++++++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 91ce84637d..172f7555ac 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -129,12 +129,9 @@ class Listener: """Create new project into OP DB.""" # Use update process to avoid duplicating code - self._update_project(data) + self._update_project(data, new_project=True) - # Print message - # - Happens in write_project_to_op() - - def _update_project(self, data): + def _update_project(self, data, new_project=False): """Update project into OP DB.""" # Get project entity project = gazu.project.get_project(data["project_id"]) @@ -147,6 +144,9 @@ class Listener: data["project_id"]) self.dbcon.bulk_write([update_project]) + if new_project: + log.info("Project created: {}".format(project["name"])) + def _delete_project(self, data): """Delete project.""" @@ -579,7 +579,7 @@ class Listener: shot=task["entity"]["name"] ) - ent_type=ent_type + " - " if ent_type is not None else "" + ent_type = ent_type + " - " if ent_type is not None else "" msg = "Task deleted: {proj} - {ent_type}{parent}" \ " - {task}".format( proj=task["zou"]["project"]["name"], diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 6590d05a82..a559d8a19f 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -291,7 +291,6 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: project_name = project["name"] project_dict = get_project(project_name) if not project_dict: - log.info("Project created: {}".format(project_name)) project_dict = create_project(project_name, project_name) # Project data and tasks @@ -405,12 +404,16 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): ] # Sync project. Create if doesn't exist + project_name = project["name"] + project_dict = get_project(project_name) + if not project_dict: + log.info("Project created: {}".format(project_name)) bulk_writes.append(write_project_to_op(project, dbcon)) # Try to find project document - project_name = project["name"] + if not project_dict: + project_dict = get_project(project_name) dbcon.Session["AVALON_PROJECT"] = project_name - project_dict = get_project(project_name) # Query all assets of the local project zou_ids_and_asset_docs = { From 5a9ea0d130ba6009804ac479e3ebe6ef4ff46906 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 12:36:57 +0100 Subject: [PATCH 099/160] Changed back from dict to doc for var names --- .../modules/kitsu/utils/update_op_with_zou.py | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a559d8a19f..c215126dac 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -65,32 +65,32 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( dbcon: AvalonMongoDB, gazu_project: dict, - project_dict: dict, + project_doc: dict, entities_list: List[dict], - asset_dict_ids: Dict[str, dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. Args: dbcon (AvalonMongoDB): Connection to DB - gazu_project dict): Dict of gazu, - project_dict dict): Dict of project, + gazu_project (dict): Dict of gazu, + project_doc (dict): Dict of project, entities_list (List[dict]): List of zou entities to update - asset_dict_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...] + asset_doc_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...] Returns: List[Dict[str, dict]]: List of (doc_id, update_dict) tuples """ - if not project_dict: + if not project_doc: return - project_name = project_dict["name"] + project_name = project_doc["name"] assets_with_update = [] for item in entities_list: # Check asset exists - item_doc = asset_dict_ids.get(item["id"]) + item_doc = asset_doc_ids.get(item["id"]) if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) @@ -108,7 +108,7 @@ def update_op_assets( try: frame_in = int( item_data.pop( - "frame_in", project_dict["data"].get("frameStart") + "frame_in", project_doc["data"].get("frameStart") ) ) except (TypeError, ValueError): @@ -127,14 +127,14 @@ def update_op_assets( if frames_duration: frame_out = frame_in + frames_duration - 1 else: - frame_out = project_dict["data"].get("frameEnd", frame_in) + frame_out = project_doc["data"].get("frameEnd", frame_in) item_data["frameEnd"] = frame_out # Fps, fallback to project's value or default value (25.0) try: fps = float(item_data.get("fps")) except (TypeError, ValueError): fps = float(gazu_project.get( - "fps", project_dict["data"].get("fps", 25))) + "fps", project_doc["data"].get("fps", 25))) item_data["fps"] = fps # Resolution, fall back to project default match_res = re.match( @@ -145,27 +145,27 @@ def update_op_assets( item_data["resolutionWidth"] = int(match_res.group(1)) item_data["resolutionHeight"] = int(match_res.group(2)) else: - item_data["resolutionWidth"] = project_dict["data"].get( + item_data["resolutionWidth"] = project_doc["data"].get( "resolutionWidth") - item_data["resolutionHeight"] = project_dict["data"].get( + item_data["resolutionHeight"] = project_doc["data"].get( "resolutionHeight") # Properties that doesn't fully exist in Kitsu. # Guessing those property names below: # Pixel Aspect Ratio item_data["pixelAspect"] = item_data.get( - "pixel_aspect", project_dict["data"].get("pixelAspect")) + "pixel_aspect", project_doc["data"].get("pixelAspect")) # Handle Start item_data["handleStart"] = item_data.get( - "handle_start", project_dict["data"].get("handleStart")) + "handle_start", project_doc["data"].get("handleStart")) # Handle End item_data["handleEnd"] = item_data.get( - "handle_end", project_dict["data"].get("handleEnd")) + "handle_end", project_doc["data"].get("handleEnd")) # Clip In item_data["clipIn"] = item_data.get( - "clip_in", project_dict["data"].get("clipIn")) + "clip_in", project_doc["data"].get("clipIn")) # Clip Out item_data["clipOut"] = item_data.get( - "clip_out", project_dict["data"].get("clipOut")) + "clip_out", project_doc["data"].get("clipOut")) # Tasks tasks_list = [] @@ -209,7 +209,7 @@ def update_op_assets( # Root parent folder if exist visual_parent_doc_id = None if parent_zou_id is not None: - parent_zou_id_dict = asset_dict_ids.get(parent_zou_id) + parent_zou_id_dict = asset_doc_ids.get(parent_zou_id) if parent_zou_id_dict is not None: visual_parent_doc_id = ( parent_zou_id_dict.get("_id") @@ -233,7 +233,7 @@ def update_op_assets( item_data["parents"] = [] ancestor_id = parent_zou_id while ancestor_id is not None: - parent_doc = asset_dict_ids.get(ancestor_id) + parent_doc = asset_doc_ids.get(ancestor_id) if parent_doc is not None: item_data["parents"].insert(0, parent_doc["name"]) @@ -250,7 +250,7 @@ def update_op_assets( item_name = f"{item_data['parents'][-1]}_{item['name']}" # Update doc name - asset_dict_ids[item["id"]]["name"] = item_name + asset_doc_ids[item["id"]]["name"] = item_name else: item_name = item["name"] @@ -269,7 +269,7 @@ def update_op_assets( "$set": { "name": item_name, "data": item_data, - "parent": project_dict["_id"], + "parent": project_doc["_id"], } }, ) From 93eb9fce8609b0d6543a1f91ddb84907f89b63a3 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Thu, 2 Mar 2023 12:38:37 +0100 Subject: [PATCH 100/160] Update openpype/modules/kitsu/utils/sync_service.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix David --- openpype/modules/kitsu/utils/sync_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 172f7555ac..1efebb2d47 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -178,10 +178,11 @@ class Listener: self._update_asset(data) # Print message - ep = None ep_id = asset.get("episode_id") if ep_id and ep_id != "": ep = gazu.asset.get_episode(ep_id) + else: + ep = None msg = "Asset created: {proj_name} - {ep_name}" \ "{asset_type_name} - {asset_name}".format( From 95e1f95bc1896547bde026f5c2a2517103e9e8e8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 14:05:36 +0100 Subject: [PATCH 101/160] Moved all ep_dict code into one function --- openpype/modules/kitsu/utils/sync_service.py | 35 ++++++++------------ 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 1efebb2d47..d6bdb5391e 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -124,6 +124,11 @@ class Listener: log.info("Listening to Kitsu events...") gazu.events.run_client(self.event_client) + def get_ep_dict(self, ep_id): + if ep_id and ep_id != "": + return gazu.entity.get_entity(ep_id) + return + # == Project == def _new_project(self, data): """Create new project into OP DB.""" @@ -179,10 +184,7 @@ class Listener: # Print message ep_id = asset.get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) - else: - ep = None + ep = self.get_ep_dict(ep_id) msg = "Asset created: {proj_name} - {ep_name}" \ "{asset_type_name} - {asset_name}".format( @@ -233,10 +235,8 @@ class Listener: ) # Print message - ep = None ep_id = asset["data"]["zou"].get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) + ep = self.get_ep_dict(ep_id) msg = "Asset deleted: {proj_name} - {ep_name}" \ "{asset_type_name} - {asset_name}".format( @@ -334,10 +334,8 @@ class Listener: self._update_sequence(data) # Print message - ep = None ep_id = sequence.get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) + ep = self.get_ep_dict(ep_id) msg = "Sequence created: {proj_name} - {ep_name}" \ "{sequence_name}".format( @@ -386,10 +384,8 @@ class Listener: ) # Print message - ep = None ep_id = sequence["data"]["zou"].get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) + ep = self.get_ep_dict(ep_id) gazu_project = gazu.project.get_project( sequence["data"]["zou"]["project_id"]) @@ -418,9 +414,8 @@ class Listener: self._update_shot(data) # Print message - ep = None - if shot["episode_id"] and shot["episode_id"] != "": - ep = gazu.asset.get_episode(shot["episode_id"]) + ep_id = shot["episode_id"] + ep = self.get_ep_dict(ep_id) msg = "Shot created: {proj_name} - {ep_name}" \ "{sequence_name} - {shot_name}".format( @@ -471,10 +466,8 @@ class Listener: ) # Print message - ep = None ep_id = shot["data"]["zou"].get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) + ep = self.get_ep_dict(ep_id) msg = "Shot deleted: {proj_name} - {ep_name}" \ "{sequence_name} - {shot_name}".format( @@ -496,10 +489,8 @@ class Listener: task = gazu.task.get_task(data["task_id"]) # Print message - ep = None ep_id = task.get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) + ep = self.get_ep_dict(ep_id) parent_name = None ent_type = None From 966ba0166e349b2882cb5db1f686b6235abbd44b Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 14:07:14 +0100 Subject: [PATCH 102/160] Fixed delete_task msg creation to work with assets and episodes --- openpype/modules/kitsu/utils/sync_service.py | 41 ++++++++++---------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index d6bdb5391e..893d6a8b5e 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -554,31 +554,32 @@ class Listener: # Print message entity = gazu.entity.get_entity(task["zou"]["entity_id"]) - ep = None - ep_id = entity.get("episode_id") - if ep_id and ep_id != "": - ep = gazu.asset.get_episode(ep_id) + if entity["type"] == "Asset": + ep = self.get_ep_dict(entity["source_id"]) + + parent_name = "{ep}{entity_type} - {entity}".format( + ep=ep["name"] + " - " if ep is not None else "", + entity_type=task["zou"]["entity_type_name"], + entity=task["zou"]["entity_name"] + ) + elif entity["type"] == "Shot": + shot_dict = gazu.entity.get_entity( + task["zou"]["entity_id"]) + seq_dict = gazu.entity.get_entity( + shot_dict["parent_id"]) + ep = self.get_ep_dict(seq_dict["parent_id"]) - parent_name = None - ent_type = None - if task["task_type"]["for_entity"] == "Asset": - parent_name = task["entity"]["name"] - ent_type = task["entity_type"]["name"] - elif task["task_type"]["for_entity"] == "Shot": parent_name = "{ep}{sequence} - {shot}".format( ep=ep["name"] + " - " if ep is not None else "", - sequence=task["sequence"]["name"], - shot=task["entity"]["name"] + sequence=seq_dict["name"], + shot=shot_dict["name"] ) - ent_type = ent_type + " - " if ent_type is not None else "" - msg = "Task deleted: {proj} - {ent_type}{parent}" \ - " - {task}".format( - proj=task["zou"]["project"]["name"], - ent_type=ent_type, - parent=parent_name, - task=task["type"] - ) + msg = "Task deleted: {proj} - {parent} - {task}".format( + proj=task["zou"]["project_name"], + parent=parent_name, + task=task["zou"]["task_type_name"] + ) log.info(msg) return From 804cdcafd6268d84a3c7d2e887c9e05e5798dec4 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 19:33:11 +0100 Subject: [PATCH 103/160] Store the gazu asset data in OPs DB as sync_service does This isn't the most optimal way to do it but it makes sure the data is consistent through out the code until we can revision sync_service to only use the ID from the dict. --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index c215126dac..6797df6344 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -175,7 +175,7 @@ def update_op_assets( elif item_type == "Shot": tasks_list = gazu.task.all_tasks_for_shot(item) item_data["tasks"] = { - t["task_type_name"]: {"type": t["task_type_name"], "zou": t} + t["task_type_name"]: {"type": t["task_type_name"], "zou": gazu.task.get_task(t["id"])} for t in tasks_list } From 7176be9f92e71ee6942971e25ffc72199c1eecf8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Mar 2023 19:35:07 +0100 Subject: [PATCH 104/160] Log msg for new_task now work for both shot and assets --- openpype/modules/kitsu/utils/sync_service.py | 37 ++++++++++---------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 893d6a8b5e..1f12217d44 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -239,10 +239,10 @@ class Listener: ep = self.get_ep_dict(ep_id) msg = "Asset deleted: {proj_name} - {ep_name}" \ - "{asset_type_name} - {asset_name}".format( + "{type_name} - {asset_name}".format( proj_name=asset["data"]["zou"]["project_name"], ep_name=ep["name"] + " - " if ep is not None else "", - asset_type_name=asset["data"]["zou"]["asset_type_name"], + type_name=asset["data"]["zou"]["asset_type_name"], asset_name=asset["name"] ) log.info(msg) @@ -390,7 +390,7 @@ class Listener: gazu_project = gazu.project.get_project( sequence["data"]["zou"]["project_id"]) - msg = "Sequence created: {proj_name} - {ep_name}" \ + msg = "Sequence deleted: {proj_name} - {ep_name}" \ "{sequence_name}".format( proj_name=gazu_project["name"], ep_name=ep["name"] + " - " if ep is not None else "", @@ -493,9 +493,12 @@ class Listener: ep = self.get_ep_dict(ep_id) parent_name = None + asset_name = None ent_type = None + if task["task_type"]["for_entity"] == "Asset": parent_name = task["entity"]["name"] + asset_name = task["entity"]["name"] ent_type = task["entity_type"]["name"] elif task["task_type"]["for_entity"] == "Shot": parent_name = "{ep_name}{sequence_name} - {shot_name}".format( @@ -503,9 +506,14 @@ class Listener: sequence_name=task["sequence"]["name"], shot_name=task["entity"]["name"] ) + asset_name = "{ep_name}{sequence_name}_{shot_name}".format( + ep_name=ep["name"] + "_" if ep is not None else "", + sequence_name=task["sequence"]["name"], + shot_name=task["entity"]["name"] + ) # Update asset tasks with new one - asset_doc = get_asset_by_name(project_name, parent_name) + asset_doc = get_asset_by_name(project_name, asset_name) if asset_doc: asset_tasks = asset_doc["data"].get("tasks") task_type_name = task["task_type"]["name"] @@ -553,32 +561,25 @@ class Listener: # Print message entity = gazu.entity.get_entity(task["zou"]["entity_id"]) + ep = self.get_ep_dict(entity["source_id"]) if entity["type"] == "Asset": - ep = self.get_ep_dict(entity["source_id"]) - parent_name = "{ep}{entity_type} - {entity}".format( ep=ep["name"] + " - " if ep is not None else "", - entity_type=task["zou"]["entity_type_name"], - entity=task["zou"]["entity_name"] + entity_type=task["zou"]["entity_type"]["name"], + entity=task["zou"]["entity"]["name"] ) elif entity["type"] == "Shot": - shot_dict = gazu.entity.get_entity( - task["zou"]["entity_id"]) - seq_dict = gazu.entity.get_entity( - shot_dict["parent_id"]) - ep = self.get_ep_dict(seq_dict["parent_id"]) - parent_name = "{ep}{sequence} - {shot}".format( ep=ep["name"] + " - " if ep is not None else "", - sequence=seq_dict["name"], - shot=shot_dict["name"] + sequence=task["zou"]["sequence"]["name"], + shot=task["zou"]["entity"]["name"] ) msg = "Task deleted: {proj} - {parent} - {task}".format( - proj=task["zou"]["project_name"], + proj=task["zou"]["project"]["name"], parent=parent_name, - task=task["zou"]["task_type_name"] + task=name ) log.info(msg) From c50678bcb8c0a1cb2696fbb526d61cbe4261a361 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Fri, 3 Mar 2023 10:34:17 +0100 Subject: [PATCH 105/160] Update openpype/modules/kitsu/utils/update_op_with_zou.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix David --- openpype/modules/kitsu/utils/update_op_with_zou.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 6797df6344..73b7a4249d 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -175,7 +175,12 @@ def update_op_assets( elif item_type == "Shot": tasks_list = gazu.task.all_tasks_for_shot(item) item_data["tasks"] = { - t["task_type_name"]: {"type": t["task_type_name"], "zou": gazu.task.get_task(t["id"])} + item_data["tasks"] = { + t["task_type_name"]: { + "type": t["task_type_name"], + "zou": gazu.task.get_task(t["id"]), + } + } for t in tasks_list } From 8fc6978ea2f56778d794e213c541f89888b24795 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Mar 2023 11:10:12 +0100 Subject: [PATCH 106/160] Formatted with Black --- .../kitsu/actions/launcher_show_in_kitsu.py | 65 +++++----- .../publish/collect_kitsu_credential.py | 1 - .../plugins/publish/collect_kitsu_entities.py | 11 +- .../plugins/publish/integrate_kitsu_note.py | 5 +- .../plugins/publish/integrate_kitsu_review.py | 1 - openpype/modules/kitsu/utils/sync_service.py | 114 ++++++++++++------ .../modules/kitsu/utils/update_zou_with_op.py | 9 +- 7 files changed, 123 insertions(+), 83 deletions(-) diff --git a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py index 11224f6e52..81d98cfffb 100644 --- a/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py +++ b/openpype/modules/kitsu/actions/launcher_show_in_kitsu.py @@ -23,36 +23,37 @@ class ShowInKitsu(LauncherAction): return True def process(self, session, **kwargs): - # Context inputs project_name = session["AVALON_PROJECT"] asset_name = session.get("AVALON_ASSET", None) task_name = session.get("AVALON_TASK", None) - project = get_project(project_name=project_name, - fields=["data.zou_id"]) + project = get_project( + project_name=project_name, fields=["data.zou_id"] + ) if not project: raise RuntimeError("Project {} not found.".format(project_name)) project_zou_id = project["data"].get("zou_id") if not project_zou_id: raise RuntimeError( - "Project {} has no connected kitsu id.".format(project_name)) + "Project {} has no connected kitsu id.".format(project_name) + ) asset_zou_name = None asset_zou_id = None - asset_zou_type = 'Assets' + asset_zou_type = "Assets" task_zou_id = None - zou_sub_type = ['AssetType', 'Sequence'] + zou_sub_type = ["AssetType", "Sequence"] if asset_name: asset_zou_name = asset_name asset_fields = ["data.zou.id", "data.zou.type"] if task_name: asset_fields.append("data.tasks.{}.zou.id".format(task_name)) - asset = get_asset_by_name(project_name, - asset_name=asset_name, - fields=asset_fields) + asset = get_asset_by_name( + project_name, asset_name=asset_name, fields=asset_fields + ) asset_zou_data = asset["data"].get("zou") @@ -68,37 +69,43 @@ class ShowInKitsu(LauncherAction): task_zou_data = task_data.get("zou", {}) if not task_zou_data: self.log.debug( - "No zou task data for task: {}".format(task_name)) + "No zou task data for task: {}".format(task_name) + ) task_zou_id = task_zou_data["id"] # Define URL - url = self.get_url(project_id=project_zou_id, - asset_name=asset_zou_name, - asset_id=asset_zou_id, - asset_type=asset_zou_type, - task_id=task_zou_id) + url = self.get_url( + project_id=project_zou_id, + asset_name=asset_zou_name, + asset_id=asset_zou_id, + asset_type=asset_zou_type, + task_id=task_zou_id, + ) # Open URL in webbrowser self.log.info("Opening URL: {}".format(url)) - webbrowser.open(url, - # Try in new tab - new=2) + webbrowser.open( + url, + # Try in new tab + new=2, + ) - def get_url(self, - project_id, - asset_name=None, - asset_id=None, - asset_type=None, - task_id=None): - - shots_url = {'Shots', 'Sequence', 'Shot'} - sub_type = {'AssetType', 'Sequence'} + def get_url( + self, + project_id, + asset_name=None, + asset_id=None, + asset_type=None, + task_id=None, + ): + shots_url = {"Shots", "Sequence", "Shot"} + sub_type = {"AssetType", "Sequence"} kitsu_module = self.get_kitsu_module() # Get kitsu url with /api stripped kitsu_url = kitsu_module.server_url if kitsu_url.endswith("/api"): - kitsu_url = kitsu_url[:-len("/api")] + kitsu_url = kitsu_url[: -len("/api")] sub_url = f"/productions/{project_id}" asset_type_url = "shots" if asset_type in shots_url else "assets" @@ -121,6 +128,6 @@ class ShowInKitsu(LauncherAction): # Add search method if is a sub_type sub_url += f"/{asset_type_url}" if asset_type in sub_type: - sub_url += f'?search={asset_name}' + sub_url += f"?search={asset_name}" return f"{kitsu_url}{sub_url}" diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py index b7f6f67a40..ac501dd47d 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py @@ -13,6 +13,5 @@ class CollectKitsuSession(pyblish.api.ContextPlugin): # rename log in # families = ["kitsu"] def process(self, context): - gazu.client.set_host(os.environ["KITSU_SERVER"]) gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 71ed563580..a0bd2b305b 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -10,9 +10,9 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): label = "Kitsu entities" def process(self, context): - kitsu_project = gazu.project.get_project_by_name( - context.data["projectName"]) + context.data["projectName"] + ) if not kitsu_project: raise ValueError("Project not found in kitsu!") @@ -35,7 +35,8 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): zou_task_data = asset_doc["data"]["tasks"][task_name].get("zou") self.log.debug( - "Collected zou task data: {}".format(zou_task_data)) + "Collected zou task data: {}".format(zou_task_data) + ) entity_id = zou_asset_data["id"] entity = kitsu_entities_by_id.get(entity_id) @@ -44,7 +45,9 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): if not entity: raise ValueError( "{} was not found in kitsu!".format( - zou_asset_data["name"])) + zou_asset_data["name"] + ) + ) kitsu_entities_by_id[entity_id] = entity instance.data["entity"] = entity diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 006f0bc6d0..6702cbe7aa 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -13,7 +13,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): note_status_shortname = "wfa" def process(self, context): - # Get comment text body publish_comment = context.data.get("comment") if not publish_comment: @@ -45,9 +44,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # Add comment to kitsu task task_id = kitsu_task["id"] - self.log.debug( - "Add new note in taks id {}".format(task_id) - ) + self.log.debug("Add new note in taks id {}".format(task_id)) kitsu_comment = gazu.task.add_comment( task_id, note_status, comment=publish_comment ) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index d8f6cb7ac8..12482b5657 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -12,7 +12,6 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): optional = True def process(self, instance): - task = instance.data["kitsu_task"]["id"] comment = instance.data["kitsu_comment"]["id"] diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 1f12217d44..34714fa4b3 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -102,7 +102,8 @@ class Listener: ) gazu.events.add_listener( - self.event_client, "shot:new", self._new_shot) + self.event_client, "shot:new", self._new_shot + ) gazu.events.add_listener( self.event_client, "shot:update", self._update_shot ) @@ -111,7 +112,8 @@ class Listener: ) gazu.events.add_listener( - self.event_client, "task:new", self._new_task) + self.event_client, "task:new", self._new_task + ) gazu.events.add_listener( self.event_client, "task:update", self._update_task ) @@ -146,7 +148,8 @@ class Listener: # Write into DB if update_project: self.dbcon.Session["AVALON_PROJECT"] = get_kitsu_project_name( - data["project_id"]) + data["project_id"] + ) self.dbcon.bulk_write([update_project]) if new_project: @@ -158,7 +161,8 @@ class Listener: collections = self.dbcon.database.list_collection_names() for collection in collections: project = self.dbcon.database[collection].find_one( - {"data.zou_id": data["project_id"]}) + {"data.zou_id": data["project_id"]} + ) if project: # Delete project collection self.dbcon.database[project["name"]].drop() @@ -186,13 +190,15 @@ class Listener: ep_id = asset.get("episode_id") ep = self.get_ep_dict(ep_id) - msg = "Asset created: {proj_name} - {ep_name}" \ + msg = ( + "Asset created: {proj_name} - {ep_name}" "{asset_type_name} - {asset_name}".format( proj_name=asset["project_name"], ep_name=ep["name"] + " - " if ep is not None else "", asset_type_name=asset["asset_type_name"], - asset_name=asset["name"] + asset_name=asset["name"], ) + ) log.info(msg) def _update_asset(self, data): @@ -216,8 +222,11 @@ class Listener: # Update update_op_result = update_op_assets( - self.dbcon, gazu_project, project_doc, - [asset], zou_ids_and_asset_docs + self.dbcon, + gazu_project, + project_doc, + [asset], + zou_ids_and_asset_docs, ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -238,13 +247,15 @@ class Listener: ep_id = asset["data"]["zou"].get("episode_id") ep = self.get_ep_dict(ep_id) - msg = "Asset deleted: {proj_name} - {ep_name}" \ + msg = ( + "Asset deleted: {proj_name} - {ep_name}" "{type_name} - {asset_name}".format( proj_name=asset["data"]["zou"]["project_name"], ep_name=ep["name"] + " - " if ep is not None else "", type_name=asset["data"]["zou"]["asset_type_name"], - asset_name=asset["name"] + asset_name=asset["name"], ) + ) log.info(msg) # == Episode == @@ -264,8 +275,7 @@ class Listener: # Print message msg = "Episode created: {proj_name} - {ep_name}".format( - proj_name=ep["project_name"], - ep_name=ep["name"] + proj_name=ep["project_name"], ep_name=ep["name"] ) log.info(msg) @@ -290,8 +300,11 @@ class Listener: # Update update_op_result = update_op_assets( - self.dbcon, gazu_project, project_doc, - [ep], zou_ids_and_asset_docs + self.dbcon, + gazu_project, + project_doc, + [ep], + zou_ids_and_asset_docs, ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -310,11 +323,11 @@ class Listener: # Print message project = gazu.project.get_project( - ep["data"]["zou"]["project_id"]) + ep["data"]["zou"]["project_id"] + ) msg = "Episode deleted: {proj_name} - {ep_name}".format( - proj_name=project["name"], - ep_name=ep["name"] + proj_name=project["name"], ep_name=ep["name"] ) log.info(msg) @@ -337,12 +350,14 @@ class Listener: ep_id = sequence.get("episode_id") ep = self.get_ep_dict(ep_id) - msg = "Sequence created: {proj_name} - {ep_name}" \ + msg = ( + "Sequence created: {proj_name} - {ep_name}" "{sequence_name}".format( proj_name=sequence["project_name"], ep_name=ep["name"] + " - " if ep is not None else "", - sequence_name=sequence["name"] + sequence_name=sequence["name"], ) + ) log.info(msg) def _update_sequence(self, data): @@ -366,8 +381,11 @@ class Listener: # Update update_op_result = update_op_assets( - self.dbcon, gazu_project, project_doc, - [sequence], zou_ids_and_asset_docs + self.dbcon, + gazu_project, + project_doc, + [sequence], + zou_ids_and_asset_docs, ) if update_op_result: asset_doc_id, asset_update = update_op_result[0] @@ -388,14 +406,17 @@ class Listener: ep = self.get_ep_dict(ep_id) gazu_project = gazu.project.get_project( - sequence["data"]["zou"]["project_id"]) + sequence["data"]["zou"]["project_id"] + ) - msg = "Sequence deleted: {proj_name} - {ep_name}" \ + msg = ( + "Sequence deleted: {proj_name} - {ep_name}" "{sequence_name}".format( proj_name=gazu_project["name"], ep_name=ep["name"] + " - " if ep is not None else "", - sequence_name=sequence["name"] + sequence_name=sequence["name"], ) + ) log.info(msg) # == Shot == @@ -417,13 +438,15 @@ class Listener: ep_id = shot["episode_id"] ep = self.get_ep_dict(ep_id) - msg = "Shot created: {proj_name} - {ep_name}" \ + msg = ( + "Shot created: {proj_name} - {ep_name}" "{sequence_name} - {shot_name}".format( proj_name=shot["project_name"], ep_name=ep["name"] + " - " if ep is not None else "", sequence_name=shot["sequence_name"], - shot_name=shot["name"] + shot_name=shot["name"], ) + ) log.info(msg) def _update_shot(self, data): @@ -440,14 +463,18 @@ class Listener: zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in get_assets(project_name) - if asset_doc["data"].get("zou", {}).get("id")} + if asset_doc["data"].get("zou", {}).get("id") + } zou_ids_and_asset_docs[shot["project_id"]] = project_doc gazu_project = gazu.project.get_project(shot["project_id"]) # Update update_op_result = update_op_assets( - self.dbcon, gazu_project, project_doc, - [shot], zou_ids_and_asset_docs + self.dbcon, + gazu_project, + project_doc, + [shot], + zou_ids_and_asset_docs, ) if update_op_result: @@ -469,13 +496,15 @@ class Listener: ep_id = shot["data"]["zou"].get("episode_id") ep = self.get_ep_dict(ep_id) - msg = "Shot deleted: {proj_name} - {ep_name}" \ + msg = ( + "Shot deleted: {proj_name} - {ep_name}" "{sequence_name} - {shot_name}".format( proj_name=shot["data"]["zou"]["project_name"], ep_name=ep["name"] + " - " if ep is not None else "", sequence_name=shot["data"]["zou"]["sequence_name"], - shot_name=shot["name"] + shot_name=shot["name"], ) + ) log.info(msg) # == Task == @@ -504,12 +533,12 @@ class Listener: parent_name = "{ep_name}{sequence_name} - {shot_name}".format( ep_name=ep["name"] + " - " if ep is not None else "", sequence_name=task["sequence"]["name"], - shot_name=task["entity"]["name"] + shot_name=task["entity"]["name"], ) asset_name = "{ep_name}{sequence_name}_{shot_name}".format( ep_name=ep["name"] + "_" if ep is not None else "", sequence_name=task["sequence"]["name"], - shot_name=task["entity"]["name"] + shot_name=task["entity"]["name"], ) # Update asset tasks with new one @@ -518,20 +547,24 @@ class Listener: asset_tasks = asset_doc["data"].get("tasks") task_type_name = task["task_type"]["name"] asset_tasks[task_type_name] = { - "type": task_type_name, "zou": task} + "type": task_type_name, + "zou": task, + } self.dbcon.update_one( {"_id": asset_doc["_id"]}, - {"$set": {"data.tasks": asset_tasks}} + {"$set": {"data.tasks": asset_tasks}}, ) # Print message - msg = "Task created: {proj} - {ent_type}{parent}" \ + msg = ( + "Task created: {proj} - {ent_type}{parent}" " - {task}".format( proj=task["project"]["name"], ent_type=ent_type + " - " if ent_type is not None else "", parent=parent_name, - task=task["task_type"]["name"] + task=task["task_type"]["name"], ) + ) log.info(msg) def _update_task(self, data): @@ -567,19 +600,19 @@ class Listener: parent_name = "{ep}{entity_type} - {entity}".format( ep=ep["name"] + " - " if ep is not None else "", entity_type=task["zou"]["entity_type"]["name"], - entity=task["zou"]["entity"]["name"] + entity=task["zou"]["entity"]["name"], ) elif entity["type"] == "Shot": parent_name = "{ep}{sequence} - {shot}".format( ep=ep["name"] + " - " if ep is not None else "", sequence=task["zou"]["sequence"]["name"], - shot=task["zou"]["entity"]["name"] + shot=task["zou"]["entity"]["name"], ) msg = "Task deleted: {proj} - {parent} - {task}".format( proj=task["zou"]["project"]["name"], parent=parent_name, - task=name + task=name, ) log.info(msg) @@ -593,6 +626,7 @@ def start_listeners(login: str, password: str): login (str): Kitsu user login password (str): Kitsu user password """ + # Refresh token every week def refresh_token_every_week(): log.info("Refreshing token...") diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 617f037c1e..be931af233 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -83,7 +83,8 @@ def sync_zou_from_op_project( } ) gazu.project.update_project_data( - zou_project, data=project_doc["data"]) + zou_project, data=project_doc["data"] + ) gazu.project.update_project(zou_project) asset_types = gazu.asset.all_asset_types() @@ -99,8 +100,7 @@ def sync_zou_from_op_project( project_module_settings = get_project_settings(project_name)["kitsu"] dbcon.Session["AVALON_PROJECT"] = project_name asset_docs = { - asset_doc["_id"]: asset_doc - for asset_doc in get_assets(project_name) + asset_doc["_id"]: asset_doc for asset_doc in get_assets(project_name) } # Create new assets @@ -176,7 +176,8 @@ def sync_zou_from_op_project( frame_in=doc["data"]["frameStart"], frame_out=doc["data"]["frameEnd"], nb_frames=( - doc["data"]["frameEnd"] - doc["data"]["frameStart"] + 1), + doc["data"]["frameEnd"] - doc["data"]["frameStart"] + 1 + ), ) elif match.group(2): # Sequence From 67bc287321fd03287aedf222e6d9c7ebf25e3332 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Mar 2023 11:12:17 +0100 Subject: [PATCH 107/160] Fix hound comments --- openpype/modules/kitsu/utils/update_op_with_zou.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 73b7a4249d..053e803ff3 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -175,12 +175,12 @@ def update_op_assets( elif item_type == "Shot": tasks_list = gazu.task.all_tasks_for_shot(item) item_data["tasks"] = { - item_data["tasks"] = { - t["task_type_name"]: { - "type": t["task_type_name"], - "zou": gazu.task.get_task(t["id"]), + item_data["tasks"] = { + t["task_type_name"]: { + "type": t["task_type_name"], + "zou": gazu.task.get_task(t["id"]), + } } - } for t in tasks_list } From 67e8f59935a7a1824aceb71cdc32e354c8a33a98 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 4 Mar 2023 03:27:48 +0000 Subject: [PATCH 108/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 4d6f3d43e4..2939ddbbac 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.2-nightly.3" +__version__ = "3.15.2-nightly.4" From b077815dc5adcb7c839cc77825498369402ef7af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 5 Mar 2023 10:32:13 +0100 Subject: [PATCH 109/160] Get and set openpype context data on comp --- openpype/hosts/fusion/api/pipeline.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index 2d0a1da8fa..b982e1c2e9 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -155,10 +155,12 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return ls() def update_context_data(self, data, changes): - print(data, changes) + comp = get_current_comp() + comp.SetData("openpype", data) def get_context_data(self): - return {} + comp = get_current_comp() + return comp.GetData("openpype") or {} def on_pyblish_instance_toggled(instance, old_value, new_value): From 406bc798c45fff19edee17730ffaee587a5a8b48 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 5 Mar 2023 10:39:57 +0100 Subject: [PATCH 110/160] Tweak creator updates to newer style updates --- .../fusion/plugins/create/create_saver.py | 11 +++-------- .../fusion/plugins/create/create_workfile.py | 18 ++++-------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 439064770e..777dfb2e67 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -87,15 +87,10 @@ class CreateSaver(Creator): return qtawesome.icon("fa.eye", color="white") def update_instances(self, update_list): - for update in update_list: - instance = update.instance + for created_inst, _changes in update_list: - # Get the new values after the changes by key, ignore old value - new_data = { - key: new for key, (_old, new) in update.changes.items() - } - - tool = instance.transient_data["tool"] + new_data = created_inst.data_to_store() + tool = created_inst.transient_data["tool"] self._update_tool_with_data(tool, new_data) self._imprint(tool, new_data) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 917780c56e..3f11d69425 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -53,20 +53,15 @@ class FusionWorkfileCreator(AutoCreator): self._add_instance_to_context(instance) def update_instances(self, update_list): - for update in update_list: - instance = update.instance - comp = instance.transient_data["comp"] + for created_inst, _changes in update_list: + comp = created_inst.transient_data["comp"] if not hasattr(comp, "SetData"): # Comp is not alive anymore, likely closed by the user self.log.error("Workfile comp not found for existing instance." " Comp might have been closed in the meantime.") continue - # TODO: It appears sometimes this could be 'nested' - # Get the new values after the changes by key, ignore old value - new_data = { - key: new for key, (_old, new) in update.changes.items() - } + new_data = created_inst.data_to_store() self._imprint(comp, new_data) def create(self, options=None): @@ -128,9 +123,4 @@ class FusionWorkfileCreator(AutoCreator): for key in ["variant", "subset", "asset", "task"]: data.pop(key, None) - # Flatten any potential nested dicts - data = flatten_dict(data, separator=".") - - # Prefix with data key openpype.workfile - data = {f"{self.data_key}.{key}" for key, value in data.items()} - comp.SetData(data) + comp.SetData(self.data_key, data) From 37591de2913bfadd033e904fdfb88ffee550c4ad Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 5 Mar 2023 10:40:56 +0100 Subject: [PATCH 111/160] Change workfile Creator data key so it doesn't interfere with global comp context data in any way (Fusion allows to access nested dicts (lua tables) using the dot notation) --- openpype/hosts/fusion/plugins/create/create_workfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 3f11d69425..c67a9793dd 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -33,7 +33,7 @@ class FusionWorkfileCreator(AutoCreator): create_allow_context_change = False - data_key = "openpype.workfile" + data_key = "openpype_workfile" def collect_instances(self): From 5efc9e0ff0cdf0f410b7a0b92b27cc2ed03256e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:00:02 +0100 Subject: [PATCH 112/160] Editorial: Fix tasks removal (#4558) Fix tasks removal in editorial --- .../publish/extract_hierarchy_avalon.py | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index b2a6adc210..493780645c 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -135,6 +135,38 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): ) return project_doc + def _prepare_new_tasks(self, asset_doc, entity_data): + new_tasks = entity_data.get("tasks") or {} + if not asset_doc: + return new_tasks + + old_tasks = asset_doc.get("data", {}).get("tasks") + # Just use new tasks if old are not available + if not old_tasks: + return new_tasks + + output = deepcopy(old_tasks) + # Create mapping of lowered task names from old tasks + cur_task_low_mapping = { + task_name.lower(): task_name + for task_name in old_tasks + } + # Add/update tasks from new entity data + for task_name, task_info in new_tasks.items(): + task_info = deepcopy(task_info) + task_name_low = task_name.lower() + # Add new task + if task_name_low not in cur_task_low_mapping: + output[task_name] = task_info + continue + + # Update existing task with new info + mapped_task_name = cur_task_low_mapping.pop(task_name_low) + src_task_info = output.pop(mapped_task_name) + src_task_info.update(task_info) + output[task_name] = src_task_info + return output + def sync_asset( self, asset_name, @@ -170,11 +202,12 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): data["parents"] = parents asset_doc = asset_docs_by_name.get(asset_name) + + # Tasks + data["tasks"] = self._prepare_new_tasks(asset_doc, entity_data) + # --- Create/Unarchive asset and end --- if not asset_doc: - # Just use tasks from entity data as they are - # - this is different from the case when tasks are updated - data["tasks"] = entity_data.get("tasks") or {} archived_asset_doc = None for archived_entity in archived_asset_docs_by_name[asset_name]: archived_parents = ( @@ -201,19 +234,6 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if "data" not in asset_doc: asset_doc["data"] = {} cur_entity_data = asset_doc["data"] - cur_entity_tasks = cur_entity_data.get("tasks") or {} - - # Tasks - data["tasks"] = {} - new_tasks = entity_data.get("tasks") or {} - for task_name, task_info in new_tasks.items(): - task_info = deepcopy(task_info) - if task_name in cur_entity_tasks: - src_task_info = deepcopy(cur_entity_tasks[task_name]) - src_task_info.update(task_info) - task_info = src_task_info - - data["tasks"][task_name] = task_info changes = {} for key, value in data.items(): From 08c71380709cf672e4b930b351a0671331521610 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 6 Mar 2023 11:13:57 +0100 Subject: [PATCH 113/160] Nuke: moving deepcopy to abstraction --- openpype/pipeline/colorspace.py | 31 ++++++++++---------- openpype/pipeline/publish/publish_plugins.py | 5 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 6f68bdc5bf..2085e2d37f 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -335,9 +335,10 @@ def get_imageio_config( get_template_data_from_session) anatomy_data = get_template_data_from_session() + formatting_data = deepcopy(anatomy_data) # add project roots to anatomy data - anatomy_data["root"] = anatomy.roots - anatomy_data["platform"] = platform.system().lower() + formatting_data["root"] = anatomy.roots + formatting_data["platform"] = platform.system().lower() # get colorspace settings imageio_global, imageio_host = _get_imageio_settings( @@ -347,7 +348,7 @@ def get_imageio_config( if config_host.get("enabled"): config_data = _get_config_data( - config_host["filepath"], anatomy_data + config_host["filepath"], formatting_data ) else: config_data = None @@ -356,7 +357,7 @@ def get_imageio_config( # get config path from either global or host_name config_global = imageio_global["ocio_config"] config_data = _get_config_data( - config_global["filepath"], anatomy_data + config_global["filepath"], formatting_data ) if not config_data: @@ -372,12 +373,12 @@ def _get_config_data(path_list, anatomy_data): """Return first existing path in path list. If template is used in path inputs, - then it is formated by anatomy data + then it is formatted by anatomy data and environment variables Args: path_list (list[str]): list of abs paths - anatomy_data (dict): formating data + anatomy_data (dict): formatting data Returns: dict: config data @@ -389,30 +390,30 @@ def _get_config_data(path_list, anatomy_data): # first try host config paths for path_ in path_list: - formated_path = _format_path(path_, formatting_data) + formatted_path = _format_path(path_, formatting_data) - if not os.path.exists(formated_path): + if not os.path.exists(formatted_path): continue return { - "path": os.path.normpath(formated_path), + "path": os.path.normpath(formatted_path), "template": path_ } -def _format_path(tempate_path, formatting_data): - """Single template path formating. +def _format_path(template_path, formatting_data): + """Single template path formatting. Args: - tempate_path (str): template string + template_path (str): template string formatting_data (dict): data to be used for - template formating + template formatting Returns: - str: absolute formated path + str: absolute formatted path """ # format path for anatomy keys - formatted_path = StringTemplate(tempate_path).format( + formatted_path = StringTemplate(template_path).format( formatting_data) return os.path.abspath(formatted_path) diff --git a/openpype/pipeline/publish/publish_plugins.py b/openpype/pipeline/publish/publish_plugins.py index 2df98221ba..331235fadc 100644 --- a/openpype/pipeline/publish/publish_plugins.py +++ b/openpype/pipeline/publish/publish_plugins.py @@ -1,4 +1,3 @@ -from copy import deepcopy import inspect from abc import ABCMeta from pprint import pformat @@ -311,7 +310,7 @@ class ColormanagedPyblishPluginMixin(object): @staticmethod def get_colorspace_settings(context): - """Retuns solved settings for the host context. + """Returns solved settings for the host context. Args: context (publish.Context): publishing context @@ -324,7 +323,7 @@ class ColormanagedPyblishPluginMixin(object): project_name = context.data["projectName"] host_name = context.data["hostName"] - anatomy_data = deepcopy(context.data["anatomyData"]) + anatomy_data = context.data["anatomyData"] project_settings_ = context.data["project_settings"] config_data = get_imageio_config( From ec78ebff691eec6c124dfc95c10c9760bac1d1b5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 6 Mar 2023 10:20:12 +0000 Subject: [PATCH 114/160] Add skeletalmesh family as loadable as reference --- openpype/hosts/maya/plugins/load/load_reference.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 858c9b709e..d93702a16d 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -26,6 +26,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "rig", "camerarig", "staticMesh", + "skeletalMesh", "mvLook"] representations = ["ma", "abc", "fbx", "mb"] From 84574eaca8cc8bfa707d39d411c784621754975a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 6 Mar 2023 11:50:41 +0100 Subject: [PATCH 115/160] Nuke: fix clip sequence loading --- openpype/hosts/nuke/plugins/load/load_clip.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index d170276add..cb3da79ef5 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -222,18 +222,21 @@ class LoadClip(plugin.NukeLoader): """ representation = deepcopy(representation) context = representation["context"] - template = representation["data"]["template"] + + # Get the frame from the context and hash it + frame = context["frame"] + hashed_frame = "#" * len(str(frame)) + + # Replace the frame with the hash in the originalBasename if ( - "{originalBasename}" in template - and "frame" in context + "{originalBasename}" in representation["data"]["template"] ): - frame = context["frame"] - hashed_frame = "#" * len(str(frame)) origin_basename = context["originalBasename"] context["originalBasename"] = origin_basename.replace( frame, hashed_frame ) + # Replace the frame with the hash in the frame representation["context"]["frame"] = hashed_frame return representation From b513bb437d2e48b20f305fadb4b71639724a3875 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Mar 2023 16:28:37 +0100 Subject: [PATCH 116/160] Set subset in a more correct OpenPype way --- .../hosts/fusion/plugins/create/create_saver.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 777dfb2e67..b0c0d830a3 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -12,6 +12,7 @@ from openpype.pipeline import ( Creator, CreatedInstance ) +from openpype.client import get_asset_by_name class CreateSaver(Creator): @@ -145,14 +146,22 @@ class CreateSaver(Creator): asset = legacy_io.Session["AVALON_ASSET"] task = legacy_io.Session["AVALON_TASK"] + asset_doc = get_asset_by_name(project_name=project, + asset_name=asset) + path = tool["Clip"][comp.TIME_UNDEFINED] fname = os.path.basename(path) fname, _ext = os.path.splitext(fname) - subset = fname.rstrip(".") + variant = fname.rstrip(".") + subset = self.get_subset_name( + variant=variant, + task_name=task, + asset_doc=asset_doc, + project_name=project, + ) attrs = tool.GetAttrs() passthrough = attrs["TOOLB_PassThrough"] - variant = subset[len("render"):] return { # Required data "project": project, From 99637875efa176a7e462212880a5f4c26f2b6f78 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Mar 2023 16:31:09 +0100 Subject: [PATCH 117/160] Do not secretly pop data that OP generates by default --- openpype/hosts/fusion/plugins/create/create_workfile.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index c67a9793dd..e539dcf019 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -117,10 +117,4 @@ class FusionWorkfileCreator(AutoCreator): return qtawesome.icon("fa.file-o", color="white") def _imprint(self, comp, data): - - # TODO: Should this keys persist or not? I'd prefer not - # Do not persist the current context for the Workfile - for key in ["variant", "subset", "asset", "task"]: - data.pop(key, None) - comp.SetData(self.data_key, data) From b2eb14914b6644c8d5b3797751da74c37b804e83 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 6 Mar 2023 16:55:05 +0100 Subject: [PATCH 118/160] global, nuke: adding support for first workfile creation --- openpype/hosts/nuke/api/lib.py | 9 ++-- openpype/hosts/nuke/api/pipeline.py | 2 +- .../nuke/api/workfile_template_builder.py | 1 - openpype/hosts/nuke/api/workio.py | 2 +- .../workfile/workfile_template_builder.py | 46 +++++++++++++------ 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index cd31e42690..793dc8fcdd 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2682,11 +2682,12 @@ def start_workfile_template_builder(): build_workfile_template ) - # to avoid looping of the callback, remove it! - # nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") - log.info("Starting workfile template builder...") - build_workfile_template(run_from_callback=True) + # to avoid looping of the callback, remove it! + log.info("Starting workfile template builder...") + build_workfile_template(workfile_creation_enabled=True) + + nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") @deprecated def recreate_instance(origin_node, avalon_data=None): diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 30270a4e5f..d649ffae7f 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -155,8 +155,8 @@ def add_nuke_callbacks(): # Set context settings. nuke.addOnCreate( workfile_settings.set_context_settings, nodeClass="Root") - nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") nuke.addOnCreate(workfile_settings.set_favorites, nodeClass="Root") + nuke.addOnCreate(start_workfile_template_builder, nodeClass="Root") nuke.addOnCreate(process_workfile_builder, nodeClass="Root") # fix ffmpeg settings on script diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index 80db0d160c..a6805d1b14 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -56,7 +56,6 @@ class NukeTemplateBuilder(AbstractTemplateBuilder): return True - class NukePlaceholderPlugin(PlaceholderPlugin): node_color = 4278190335 diff --git a/openpype/hosts/nuke/api/workio.py b/openpype/hosts/nuke/api/workio.py index 65b86bf01b..5692f8e63c 100644 --- a/openpype/hosts/nuke/api/workio.py +++ b/openpype/hosts/nuke/api/workio.py @@ -13,7 +13,7 @@ def has_unsaved_changes(): def save_file(filepath): path = filepath.replace("\\", "/") - nuke.scriptSaveAs(path) + nuke.scriptSaveAs(path, overwrite=1) nuke.Root()["name"].setValue(path) nuke.Root()["project_directory"].setValue(os.path.dirname(path)) nuke.Root().setModified(False) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 3dd769447f..d578114de2 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -443,7 +443,7 @@ class AbstractTemplateBuilder(object): level_limit=None, keep_placeholders=None, create_first_version=None, - run_from_callback=False + workfile_creation_enabled=False ): """Main callback for building workfile from template path. @@ -461,7 +461,7 @@ class AbstractTemplateBuilder(object): hosts to decide if they want to remove placeholder after it is used. create_first_version (bool): create first version of a workfile - run_from_callback (bool): If True, it might create first version + workfile_creation_enabled (bool): If True, it might create first version but ignore process if version is created """ @@ -475,13 +475,25 @@ class AbstractTemplateBuilder(object): if create_first_version is None: create_first_version = template_preset["create_first_version"] - # run creation of first version only if it is - # run from callback and no new version is created - first_creation = False - if create_first_version and run_from_callback: - first_creation = not self.create_first_workfile_version() + # check if first version is created + created_version_workfile = self.create_first_workfile_version() - if first_creation: + # if first version is created, import template and populate placeholders + if ( + create_first_version + and workfile_creation_enabled + and created_version_workfile + ): + self.import_template(template_path) + self.populate_scene_placeholders( + level_limit, keep_placeholders) + + # save workfile after template is populated + self.save_workfile(created_version_workfile) + + # ignore process if first workfile is enabled + # but a version is already created + if workfile_creation_enabled: return self.import_template(template_path) @@ -546,20 +558,26 @@ class AbstractTemplateBuilder(object): host's template file. """ last_workfile_path = os.environ.get("AVALON_LAST_WORKFILE") + self.log.info("__ last_workfile_path: {}".format(last_workfile_path)) if os.path.exists(last_workfile_path): # ignore in case workfile existence self.log.info("Workfile already exists, skipping creation.") return False - # Save current scene, continue to open file - if isinstance(self.host, IWorkfileHost): - self.host.save_workfile(last_workfile_path) - else: - self.host.save_file(last_workfile_path) + # Create first version + self.log.info("Creating first version of workfile.") + self.save_workfile(last_workfile_path) # Confirm creation of first version - return True + return last_workfile_path + def save_workfile(self, workfile_path): + """Save workfile in current host.""" + # Save current scene, continue to open file + if isinstance(self.host, IWorkfileHost): + self.host.save_workfile(workfile_path) + else: + self.host.save_file(workfile_path) def _prepare_placeholders(self, placeholders): """Run preparation part for placeholders on plugins. From 6d9084b14424e6c41e859ce633f8b861f6619cd2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Mar 2023 16:58:17 +0100 Subject: [PATCH 119/160] Match workfile creator logic more with the one from After Effects --- .../fusion/plugins/create/create_workfile.py | 84 ++++++++----------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index e539dcf019..19da2c36a6 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -13,17 +13,6 @@ from openpype.pipeline import ( ) -def flatten_dict(d, parent_key=None, separator="."): - items = [] - for key, v in d.items(): - new_key = parent_key + separator + key if parent_key else key - if isinstance(v, collections.MutableMapping): - items.extend(flatten_dict(v, new_key, separator=separator).items()) - else: - items.append((new_key, v)) - return dict(items) - - class FusionWorkfileCreator(AutoCreator): identifier = "workfile" family = "workfile" @@ -61,8 +50,9 @@ class FusionWorkfileCreator(AutoCreator): " Comp might have been closed in the meantime.") continue - new_data = created_inst.data_to_store() - self._imprint(comp, new_data) + # Imprint data into the comp + data = created_inst.data_to_store() + comp.SetData(self.data_key, data) def create(self, options=None): @@ -71,50 +61,50 @@ class FusionWorkfileCreator(AutoCreator): self.log.error("Unable to find current comp") return - # TODO: Is this really necessary? - # Force kill any existing "workfile" instances + existing_instance = None for instance in self.create_context.instances: if instance.family == self.family: - self.log.debug(f"Removing instance: {instance}") - self._remove_instance_from_context(instance) + existing_instance = instance + break project_name = legacy_io.Session["AVALON_PROJECT"] asset_name = legacy_io.Session["AVALON_ASSET"] task_name = legacy_io.Session["AVALON_TASK"] host_name = legacy_io.Session["AVALON_APP"] - asset_doc = get_asset_by_name(project_name, asset_name) - subset_name = self.get_subset_name( - self.default_variant, task_name, asset_doc, - project_name, host_name - ) - data = { - "asset": asset_name, - "task": task_name, - "variant": self.default_variant - } - data.update(self.get_dynamic_data( - self.default_variant, - task_name, - asset_doc, - project_name, - host_name, - data - )) + if existing_instance is None: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, task_name, asset_doc, + project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": self.default_variant + } + data.update(self.get_dynamic_data( + self.default_variant, task_name, asset_doc, + project_name, host_name, None + )) - instance = CreatedInstance( - family=self.family, - subset_name=subset_name, - data=data, - creator=self - ) - instance.transient_data["comp"] = comp - self._add_instance_to_context(instance) + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) - self._imprint(comp, data) + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, task_name, asset_doc, + project_name, host_name + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name def get_icon(self): return qtawesome.icon("fa.file-o", color="white") - - def _imprint(self, comp, data): - comp.SetData(self.data_key, data) From 52fac29164d279823a72b15a1ad5ac4c1b57b8b6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Mar 2023 17:07:15 +0100 Subject: [PATCH 120/160] Cleanup unused import --- openpype/hosts/fusion/plugins/create/create_workfile.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 19da2c36a6..2f78e4fe52 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -1,5 +1,3 @@ -import collections - import qtawesome from openpype.hosts.fusion.api import ( From af393688389346fc590a504514d4eb8de0c375ce Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 6 Mar 2023 17:07:56 +0100 Subject: [PATCH 121/160] hound --- openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/nuke/api/workfile_template_builder.py | 4 ---- openpype/pipeline/workfile/workfile_template_builder.py | 8 +++++--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 793dc8fcdd..a5a631cc70 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2682,11 +2682,11 @@ def start_workfile_template_builder(): build_workfile_template ) - # to avoid looping of the callback, remove it! log.info("Starting workfile template builder...") build_workfile_template(workfile_creation_enabled=True) + # remove callback since it would be duplicating the workfile nuke.removeOnCreate(start_workfile_template_builder, nodeClass="Root") @deprecated diff --git a/openpype/hosts/nuke/api/workfile_template_builder.py b/openpype/hosts/nuke/api/workfile_template_builder.py index a6805d1b14..fb0afb3d55 100644 --- a/openpype/hosts/nuke/api/workfile_template_builder.py +++ b/openpype/hosts/nuke/api/workfile_template_builder.py @@ -1,8 +1,5 @@ -import os import collections - import nuke - from openpype.pipeline import registered_host from openpype.pipeline.workfile.workfile_template_builder import ( AbstractTemplateBuilder, @@ -15,7 +12,6 @@ from openpype.pipeline.workfile.workfile_template_builder import ( from openpype.tools.workfile_template_build import ( WorkfileBuildPlaceholderDialog, ) -from openpype.host import IWorkfileHost from .lib import ( find_free_space_to_paste_nodes, get_extreme_positions, diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index d578114de2..0ce59de8ad 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -461,8 +461,9 @@ class AbstractTemplateBuilder(object): hosts to decide if they want to remove placeholder after it is used. create_first_version (bool): create first version of a workfile - workfile_creation_enabled (bool): If True, it might create first version - but ignore process if version is created + workfile_creation_enabled (bool): If True, it might create + first version but ignore + process if version is created """ template_preset = self.get_template_preset() @@ -478,7 +479,8 @@ class AbstractTemplateBuilder(object): # check if first version is created created_version_workfile = self.create_first_workfile_version() - # if first version is created, import template and populate placeholders + # if first version is created, import template + # and populate placeholders if ( create_first_version and workfile_creation_enabled From de50783c0435ec75a8ac7d9b29068c96a7bab8de Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Mar 2023 18:34:56 +0100 Subject: [PATCH 122/160] Nuke: Add option to use new creating system in workfile template builder (#4545) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added option to use new creating system in workfile template builder * fix spaces * use 'create' method on create context to trigger creation * fix attribute access * adding headless to creators and workfile builder abstraction * adding noqa for hound * hound --------- Co-authored-by: Jakub Jezek Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .../maya/api/workfile_template_builder.py | 2 + openpype/hosts/nuke/api/plugin.py | 6 +- .../nuke/plugins/create/create_write_image.py | 2 +- .../plugins/create/create_write_prerender.py | 2 +- .../plugins/create/create_write_render.py | 2 +- .../workfile/workfile_template_builder.py | 97 ++++++++++++++----- 6 files changed, 81 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/api/workfile_template_builder.py b/openpype/hosts/maya/api/workfile_template_builder.py index 2f550e787a..90ab6e21e0 100644 --- a/openpype/hosts/maya/api/workfile_template_builder.py +++ b/openpype/hosts/maya/api/workfile_template_builder.py @@ -22,6 +22,8 @@ PLACEHOLDER_SET = "PLACEHOLDERS_SET" class MayaTemplateBuilder(AbstractTemplateBuilder): """Concrete implementation of AbstractTemplateBuilder for maya""" + use_legacy_creators = True + def import_template(self, path): """Import template into current scene. Block if a template is already loaded. diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 6c2d4b84be..aec87be5ab 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -239,7 +239,11 @@ class NukeCreator(NewCreator): def get_pre_create_attr_defs(self): return [ - BoolDef("use_selection", label="Use selection") + BoolDef( + "use_selection", + default=not self.create_context.headless, + label="Use selection" + ) ] def get_creator_settings(self, project_settings, settings_key=None): diff --git a/openpype/hosts/nuke/plugins/create/create_write_image.py b/openpype/hosts/nuke/plugins/create/create_write_image.py index 1e23b3ad7f..d38253ab2f 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_image.py +++ b/openpype/hosts/nuke/plugins/create/create_write_image.py @@ -35,7 +35,7 @@ class CreateWriteImage(napi.NukeWriteCreator): attr_defs = [ BoolDef( "use_selection", - default=True, + default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum(), diff --git a/openpype/hosts/nuke/plugins/create/create_write_prerender.py b/openpype/hosts/nuke/plugins/create/create_write_prerender.py index 1603bf17e3..8103cb7c4d 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_prerender.py +++ b/openpype/hosts/nuke/plugins/create/create_write_prerender.py @@ -34,7 +34,7 @@ class CreateWritePrerender(napi.NukeWriteCreator): attr_defs = [ BoolDef( "use_selection", - default=True, + default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum() diff --git a/openpype/hosts/nuke/plugins/create/create_write_render.py b/openpype/hosts/nuke/plugins/create/create_write_render.py index 72fcb4f232..23efa62e36 100644 --- a/openpype/hosts/nuke/plugins/create/create_write_render.py +++ b/openpype/hosts/nuke/plugins/create/create_write_render.py @@ -31,7 +31,7 @@ class CreateWriteRender(napi.NukeWriteCreator): attr_defs = [ BoolDef( "use_selection", - default=True, + default=not self.create_context.headless, label="Use selection" ), self._get_render_target_enum() diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 119e4aaeb7..27214af79f 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -43,7 +43,8 @@ from openpype.pipeline.load import ( load_with_repre_context, ) from openpype.pipeline.create import ( - discover_legacy_creator_plugins + discover_legacy_creator_plugins, + CreateContext, ) @@ -91,6 +92,7 @@ class AbstractTemplateBuilder(object): """ _log = None + use_legacy_creators = False def __init__(self, host): # Get host name @@ -110,6 +112,7 @@ class AbstractTemplateBuilder(object): self._placeholder_plugins = None self._loaders_by_name = None self._creators_by_name = None + self._create_context = None self._system_settings = None self._project_settings = None @@ -171,6 +174,16 @@ class AbstractTemplateBuilder(object): .get("type") ) + @property + def create_context(self): + if self._create_context is None: + self._create_context = CreateContext( + self.host, + discover_publish_plugins=False, + headless=True + ) + return self._create_context + def get_placeholder_plugin_classes(self): """Get placeholder plugin classes that can be used to build template. @@ -235,18 +248,29 @@ class AbstractTemplateBuilder(object): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name + def _collect_legacy_creators(self): + creators_by_name = {} + for creator in discover_legacy_creator_plugins(): + if not creator.enabled: + continue + creator_name = creator.__name__ + if creator_name in creators_by_name: + raise KeyError( + "Duplicated creator name {} !".format(creator_name) + ) + creators_by_name[creator_name] = creator + self._creators_by_name = creators_by_name + + def _collect_creators(self): + self._creators_by_name = dict(self.create_context.creators) + def get_creators_by_name(self): if self._creators_by_name is None: - self._creators_by_name = {} - for creator in discover_legacy_creator_plugins(): - if not creator.enabled: - continue - creator_name = creator.__name__ - if creator_name in self._creators_by_name: - raise KeyError( - "Duplicated creator name {} !".format(creator_name) - ) - self._creators_by_name[creator_name] = creator + if self.use_legacy_creators: + self._collect_legacy_creators() + else: + self._collect_creators() + return self._creators_by_name def get_shared_data(self, key): @@ -1579,6 +1603,8 @@ class PlaceholderCreateMixin(object): placeholder (PlaceholderItem): Placeholder item with information about requested publishable instance. """ + + legacy_create = self.builder.use_legacy_creators creator_name = placeholder.data["creator"] create_variant = placeholder.data["create_variant"] @@ -1589,17 +1615,28 @@ class PlaceholderCreateMixin(object): task_name = legacy_io.Session["AVALON_TASK"] asset_name = legacy_io.Session["AVALON_ASSET"] - # get asset id - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) - assert asset_doc, "No current asset found in Session" - asset_id = asset_doc['_id'] + if legacy_create: + asset_doc = get_asset_by_name( + project_name, asset_name, fields=["_id"] + ) + assert asset_doc, "No current asset found in Session" + subset_name = creator_plugin.get_subset_name( + create_variant, + task_name, + asset_doc["_id"], + project_name + ) - subset_name = creator_plugin.get_subset_name( - create_variant, - task_name, - asset_id, - project_name - ) + else: + asset_doc = get_asset_by_name(project_name, asset_name) + assert asset_doc, "No current asset found in Session" + subset_name = creator_plugin.get_subset_name( + create_variant, + task_name, + asset_doc, + project_name, + self.builder.host_name + ) creator_data = { "creator_name": creator_name, @@ -1612,12 +1649,20 @@ class PlaceholderCreateMixin(object): # compile subset name from variant try: - creator_instance = creator_plugin( - subset_name, - asset_name - ).process() + if legacy_create: + creator_instance = creator_plugin( + subset_name, + asset_name + ).process() + else: + creator_instance = self.builder.create_context.create( + creator_plugin.identifier, + create_variant, + asset_doc, + task_name=task_name + ) - except Exception: + except: # noqa: E722 failed = True self.create_failed(placeholder, creator_data) From 16cece3e499b0336e490b1ac0bf01d69f715d0f6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 6 Mar 2023 19:45:17 +0100 Subject: [PATCH 123/160] Fusion: get filepath from representation instead of listing files from publish folder --- .../fusion/plugins/load/load_sequence.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/fusion/plugins/load/load_sequence.py b/openpype/hosts/fusion/plugins/load/load_sequence.py index 6f44c61d1b..9daf4b007d 100644 --- a/openpype/hosts/fusion/plugins/load/load_sequence.py +++ b/openpype/hosts/fusion/plugins/load/load_sequence.py @@ -1,11 +1,9 @@ -import os import contextlib -from openpype.client import get_version_by_id -from openpype.pipeline import ( - load, - legacy_io, - get_representation_path, +import openpype.pipeline.load as load +from openpype.pipeline.load import ( + get_representation_context, + get_representation_path_from_context ) from openpype.hosts.fusion.api import ( imprint_container, @@ -141,7 +139,7 @@ class FusionLoadSequence(load.LoaderPlugin): namespace = context['asset']['name'] # Use the first file for now - path = self._get_first_image(os.path.dirname(self.fname)) + path = get_representation_path_from_context(context) # Create the Loader with the filename path set comp = get_current_comp() @@ -210,13 +208,11 @@ class FusionLoadSequence(load.LoaderPlugin): assert tool.ID == "Loader", "Must be Loader" comp = tool.Comp() - root = os.path.dirname(get_representation_path(representation)) - path = self._get_first_image(root) + context = get_representation_context(representation) + path = get_representation_path_from_context(context) # Get start frame from version data - project_name = legacy_io.active_project() - version = get_version_by_id(project_name, representation["parent"]) - start = self._get_start(version, tool) + start = self._get_start(context["version"], tool) with comp_lock_and_undo_chunk(comp, "Update Loader"): @@ -249,11 +245,6 @@ class FusionLoadSequence(load.LoaderPlugin): with comp_lock_and_undo_chunk(comp, "Remove Loader"): tool.Delete() - def _get_first_image(self, root): - """Get first file in representation root""" - files = sorted(os.listdir(root)) - return os.path.join(root, files[0]) - def _get_start(self, version_doc, tool): """Return real start frame of published files (incl. handles)""" data = version_doc["data"] From bc1ef9229c2250aa0be84917bf6bc23e9ec65354 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Mar 2023 10:39:20 +0100 Subject: [PATCH 124/160] Photoshop: context is not changed in publisher (#4570) * OP-5025 - fix - proper changing of context When PS is already opened, new opening from different context should change it. * OP-5025 - open last workfile for new context if present * OP-5025 - remove unneeded assignemnt * OP-5025 - removed whitespace --- openpype/hosts/photoshop/api/launch_logic.py | 79 ++++++++++++++++---- 1 file changed, 63 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/photoshop/api/launch_logic.py b/openpype/hosts/photoshop/api/launch_logic.py index a4377a9972..89ba6ad4e6 100644 --- a/openpype/hosts/photoshop/api/launch_logic.py +++ b/openpype/hosts/photoshop/api/launch_logic.py @@ -10,10 +10,20 @@ from wsrpc_aiohttp import ( from qtpy import QtCore -from openpype.lib import Logger -from openpype.pipeline import legacy_io +from openpype.lib import Logger, StringTemplate +from openpype.pipeline import ( + registered_host, + Anatomy, +) +from openpype.pipeline.workfile import ( + get_workfile_template_key_from_context, + get_last_workfile, +) +from openpype.pipeline.template_data import get_template_data_with_names from openpype.tools.utils import host_tools from openpype.tools.adobe_webserver.app import WebServerTool +from openpype.pipeline.context_tools import change_current_context +from openpype.client import get_asset_by_name from .ws_stub import PhotoshopServerStub @@ -310,23 +320,28 @@ class PhotoshopRoute(WebSocketRoute): # client functions async def set_context(self, project, asset, task): """ - Sets 'project' and 'asset' to envs, eg. setting context + Sets 'project' and 'asset' to envs, eg. setting context. - Args: - project (str) - asset (str) + Opens last workile from that context if exists. + + Args: + project (str) + asset (str) + task (str """ log.info("Setting context change") - log.info("project {} asset {} ".format(project, asset)) - if project: - legacy_io.Session["AVALON_PROJECT"] = project - os.environ["AVALON_PROJECT"] = project - if asset: - legacy_io.Session["AVALON_ASSET"] = asset - os.environ["AVALON_ASSET"] = asset - if task: - legacy_io.Session["AVALON_TASK"] = task - os.environ["AVALON_TASK"] = task + log.info(f"project {project} asset {asset} task {task}") + + asset_doc = get_asset_by_name(project, asset) + change_current_context(asset_doc, task) + + last_workfile_path = self._get_last_workfile_path(project, + asset, + task) + if last_workfile_path and os.path.exists(last_workfile_path): + ProcessLauncher.execute_in_main_thread( + lambda: stub().open(last_workfile_path)) + async def read(self): log.debug("photoshop.read client calls server server calls " @@ -356,3 +371,35 @@ class PhotoshopRoute(WebSocketRoute): # Required return statement. return "nothing" + + def _get_last_workfile_path(self, project_name, asset_name, task_name): + """Returns last workfile path if exists""" + host = registered_host() + host_name = "photoshop" + template_key = get_workfile_template_key_from_context( + asset_name, + task_name, + host_name, + project_name=project_name + ) + anatomy = Anatomy(project_name) + + data = get_template_data_with_names( + project_name, asset_name, task_name, host_name + ) + data["root"] = anatomy.roots + + file_template = anatomy.templates[template_key]["file"] + + # Define saving file extension + extensions = host.get_workfile_extensions() + + folder_template = anatomy.templates[template_key]["folder"] + work_root = StringTemplate.format_strict_template( + folder_template, data + ) + last_workfile_path = get_last_workfile( + work_root, file_template, data, extensions, True + ) + + return last_workfile_path From 73e0ba9cb266507c0f7ea562d6895bcd2dbaaddb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 10:53:06 +0100 Subject: [PATCH 125/160] Set colorspace based on file rules in imageio settings --- openpype/hosts/fusion/plugins/publish/render_local.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 0eca7f6cdd..212242630b 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -1,9 +1,11 @@ import os import pyblish.api +from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk -class Fusionlocal(pyblish.api.InstancePlugin): +class Fusionlocal(pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin): """Render the current Fusion composition locally. Extract the result of savers by starting a comp render @@ -50,6 +52,11 @@ class Fusionlocal(pyblish.api.InstancePlugin): "stagingDir": output_dir, } + self.set_representation_colorspace( + representation=repre, + context=context, + ) + if "representations" not in instance.data: instance.data["representations"] = [] instance.data["representations"].append(repre) From 1bb7dbc9d9707335d246e9a7a924859c1d09fd84 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 10:55:40 +0100 Subject: [PATCH 126/160] Make sure repre preview copy is a deepcopy --- openpype/hosts/fusion/plugins/publish/render_local.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 212242630b..9ed17f23c6 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -1,4 +1,6 @@ import os +import copy + import pyblish.api from openpype.pipeline import publish from openpype.hosts.fusion.api import comp_lock_and_undo_chunk @@ -62,7 +64,7 @@ class Fusionlocal(pyblish.api.InstancePlugin, instance.data["representations"].append(repre) # review representation - repre_preview = repre.copy() + repre_preview = copy.deepcopy(repre) repre_preview["name"] = repre_preview["ext"] = "mp4" repre_preview["tags"] = ["review", "ftrackreview", "delete"] instance.data["representations"].append(repre_preview) From 70611ee884d63400e9466deaa66be7beb89d0003 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 11:36:12 +0100 Subject: [PATCH 127/160] Make sure to add the `comp` transient data for new instances --- openpype/hosts/fusion/plugins/create/create_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/plugins/create/create_workfile.py b/openpype/hosts/fusion/plugins/create/create_workfile.py index 2f78e4fe52..0bb3a0d3d4 100644 --- a/openpype/hosts/fusion/plugins/create/create_workfile.py +++ b/openpype/hosts/fusion/plugins/create/create_workfile.py @@ -89,6 +89,7 @@ class FusionWorkfileCreator(AutoCreator): new_instance = CreatedInstance( self.family, subset_name, data, self ) + new_instance.transient_data["comp"] = comp self._add_instance_to_context(new_instance) elif ( From 113b958369ae9853a3e9872a00d5c925d359b381 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 12:05:58 +0100 Subject: [PATCH 128/160] Collect Fusion workfile representation --- .../plugins/publish/collect_workfile.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 openpype/hosts/fusion/plugins/publish/collect_workfile.py diff --git a/openpype/hosts/fusion/plugins/publish/collect_workfile.py b/openpype/hosts/fusion/plugins/publish/collect_workfile.py new file mode 100644 index 0000000000..4c288edb3e --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/collect_workfile.py @@ -0,0 +1,26 @@ +import os + +import pyblish.api + + +class CollectFusionWorkfile(pyblish.api.InstancePlugin): + """Collect Fusion workfile representation.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Workfile" + hosts = ["fusion"] + families = ["workfile"] + + def process(self, instance): + + current_file = instance.context.data["currentFile"] + + folder, file = os.path.split(current_file) + filename, ext = os.path.splitext(file) + + instance.data['representations'] = [{ + 'name': ext.lstrip("."), + 'ext': ext.lstrip("."), + 'files': file, + "stagingDir": folder, + }] From b3636b9f558ace05722798e2343fcbc01ba55ca4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Mar 2023 13:45:49 +0100 Subject: [PATCH 129/160] General: Input representation ids are not ObjectIds (#4576) * input representation ids are not ObjectIds during publishing * changed set back to list * use 'setdefault' to set 'inputVersions' * added default value to 'get' * Use default value in second loop too Co-authored-by: Roy Nieterau * simplify variable assignment Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- .../fusion/plugins/publish/collect_inputs.py | 5 +---- .../houdini/plugins/publish/collect_inputs.py | 5 +---- .../hosts/maya/plugins/publish/collect_inputs.py | 4 +--- .../collect_input_representations_to_versions.py | 15 ++++++++------- 4 files changed, 11 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index 8f9857b02f..b6619fdcd6 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -1,5 +1,3 @@ -from bson.objectid import ObjectId - import pyblish.api from openpype.pipeline import registered_host @@ -108,7 +106,6 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): # Collect containers for the given set of nodes containers = collect_input_containers(nodes) - inputs = [ObjectId(c["representation"]) for c in containers] + inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/houdini/plugins/publish/collect_inputs.py b/openpype/hosts/houdini/plugins/publish/collect_inputs.py index 0b54b244bb..6411376ea3 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_inputs.py +++ b/openpype/hosts/houdini/plugins/publish/collect_inputs.py @@ -1,5 +1,3 @@ -from bson.objectid import ObjectId - import pyblish.api from openpype.pipeline import registered_host @@ -117,7 +115,6 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): # Collect containers for the given set of nodes containers = collect_input_containers(nodes) - inputs = [ObjectId(c["representation"]) for c in containers] + inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) diff --git a/openpype/hosts/maya/plugins/publish/collect_inputs.py b/openpype/hosts/maya/plugins/publish/collect_inputs.py index 470fceffc9..9c3f0f5efa 100644 --- a/openpype/hosts/maya/plugins/publish/collect_inputs.py +++ b/openpype/hosts/maya/plugins/publish/collect_inputs.py @@ -1,5 +1,4 @@ import copy -from bson.objectid import ObjectId from maya import cmds import maya.api.OpenMaya as om @@ -165,9 +164,8 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): containers = collect_input_containers(scene_containers, nodes) - inputs = [ObjectId(c["representation"]) for c in containers] + inputs = [c["representation"] for c in containers] instance.data["inputRepresentations"] = inputs - self.log.info("Collected inputs: %s" % inputs) def _collect_renderlayer_inputs(self, scene_containers, instance): diff --git a/openpype/plugins/publish/collect_input_representations_to_versions.py b/openpype/plugins/publish/collect_input_representations_to_versions.py index 18a19bce80..54a3214647 100644 --- a/openpype/plugins/publish/collect_input_representations_to_versions.py +++ b/openpype/plugins/publish/collect_input_representations_to_versions.py @@ -23,7 +23,8 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): representations = set() for instance in context: inst_repre = instance.data.get("inputRepresentations", []) - representations.update(inst_repre) + if inst_repre: + representations.update(inst_repre) representations_docs = get_representations( project_name=context.data["projectEntity"]["name"], @@ -31,7 +32,8 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): fields=["_id", "parent"]) representation_id_to_version_id = { - repre["_id"]: repre["parent"] for repre in representations_docs + str(repre["_id"]): repre["parent"] + for repre in representations_docs } for instance in context: @@ -39,9 +41,8 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): if not inst_repre: continue - input_versions = instance.data.get("inputVersions", []) + input_versions = instance.data.setdefault("inputVersions", []) for repre_id in inst_repre: - repre_id = ObjectId(repre_id) - version_id = representation_id_to_version_id[repre_id] - input_versions.append(version_id) - instance.data["inputVersions"] = input_versions + version_id = representation_id_to_version_id.get(repre_id) + if version_id: + input_versions.append(version_id) From f94fb76a238c5fc24ff26d84061fc7f7f5d5c90f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 13:56:53 +0100 Subject: [PATCH 130/160] Update OCIO config hook to use the correct imageio settings --- .../fusion/hooks/pre_fusion_ocio_hook.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py index d1ae5f64fd..6bf0f55081 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_ocio_hook.py @@ -1,7 +1,7 @@ -import os -import platform +from openpype.lib import PreLaunchHook -from openpype.lib import PreLaunchHook, ApplicationLaunchFailed +from openpype.pipeline.colorspace import get_imageio_config +from openpype.pipeline.template_data import get_template_data_with_names class FusionPreLaunchOCIO(PreLaunchHook): @@ -11,24 +11,22 @@ class FusionPreLaunchOCIO(PreLaunchHook): def execute(self): """Hook entry method.""" - # get image io - project_settings = self.data["project_settings"] + template_data = get_template_data_with_names( + project_name=self.data["project_name"], + asset_name=self.data["asset_name"], + task_name=self.data["task_name"], + host_name=self.host_name, + system_settings=self.data["system_settings"] + ) - # make sure anatomy settings are having flame key - imageio_fusion = project_settings["fusion"]["imageio"] - - ocio = imageio_fusion.get("ocio") - enabled = ocio.get("enabled", False) - if not enabled: - return - - platform_key = platform.system().lower() - ocio_path = ocio["configFilePath"][platform_key] - if not ocio_path: - raise ApplicationLaunchFailed( - "Fusion OCIO is enabled in project settings but no OCIO config" - f"path is set for your current platform: {platform_key}" - ) + config_data = get_imageio_config( + project_name=self.data["project_name"], + host_name=self.host_name, + project_settings=self.data["project_settings"], + anatomy_data=template_data, + anatomy=self.data["anatomy"] + ) + ocio_path = config_data["path"] self.log.info(f"Setting OCIO config path: {ocio_path}") - self.launch_context.env["OCIO"] = os.pathsep.join(ocio_path) + self.launch_context.env["OCIO"] = ocio_path From d47f0054deb49827890ad3b070e282d74aa2c62a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 14:38:42 +0100 Subject: [PATCH 131/160] Fix actions --- .../hosts/fusion/plugins/publish/validate_background_depth.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 261533de01..db2c4f0dd9 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -11,12 +11,11 @@ class ValidateBackgroundDepth(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Background Depth 32 bit" - actions = [RepairAction] hosts = ["fusion"] families = ["render"] optional = True - actions = [SelectInvalidAction] + actions = [SelectInvalidAction, RepairAction] @classmethod def get_invalid(cls, instance): From b4727101c969689fb8bebbfc7afde20680da7dfb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 14:39:20 +0100 Subject: [PATCH 132/160] Directly collect comp frame ranges in Collect comp --- .../fusion/plugins/publish/collect_comp.py | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp.py b/openpype/hosts/fusion/plugins/publish/collect_comp.py index dfa540fa7f..911071c9a0 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp.py @@ -1,10 +1,26 @@ -import os - import pyblish.api from openpype.hosts.fusion.api import get_current_comp +def get_comp_render_range(comp): + """Return comp's start-end render range and global start-end range.""" + comp_attrs = comp.GetAttrs() + start = comp_attrs["COMPN_RenderStart"] + end = comp_attrs["COMPN_RenderEnd"] + global_start = comp_attrs["COMPN_GlobalStart"] + global_end = comp_attrs["COMPN_GlobalEnd"] + + # Whenever render ranges are undefined fall back + # to the comp's global start and end + if start == -1000000000: + start = global_start + if end == -1000000000: + end = global_end + + return start, end, global_start, global_end + + class CollectCurrentCompFusion(pyblish.api.ContextPlugin): """Collect current comp""" @@ -15,10 +31,17 @@ class CollectCurrentCompFusion(pyblish.api.ContextPlugin): def process(self, context): """Collect all image sequence tools""" - current_comp = get_current_comp() - assert current_comp, "Must have active Fusion composition" - context.data["currentComp"] = current_comp + comp = get_current_comp() + assert comp, "Must have active Fusion composition" + context.data["currentComp"] = comp # Store path to current file - filepath = current_comp.GetAttrs().get("COMPS_FileName", "") + filepath = comp.GetAttrs().get("COMPS_FileName", "") context.data['currentFile'] = filepath + + # Store comp render ranges + start, end, global_start, global_end = get_comp_render_range(comp) + context.data["frameStart"] = int(start) + context.data["frameEnd"] = int(end) + context.data["frameStartHandle"] = int(global_start) + context.data["frameEndHandle"] = int(global_end) From a4ae05086cbbd7968b37801c3a70f0a763ab487e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 14:41:16 +0100 Subject: [PATCH 133/160] Allow to enable/disable review per saver instance + Don't create a copy of representation for review but just mark representation as review + Change Collect instances into InstancePlugin to just collect instance data per instance --- .../fusion/plugins/create/create_saver.py | 10 ++ .../plugins/publish/collect_instances.py | 122 ++++++++---------- .../fusion/plugins/publish/render_local.py | 6 +- 3 files changed, 66 insertions(+), 72 deletions(-) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index b0c0d830a3..bf11dc95c5 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -7,6 +7,7 @@ from openpype.hosts.fusion.api import ( comp_lock_and_undo_chunk ) +from openpype.lib import BoolDef from openpype.pipeline import ( legacy_io, Creator, @@ -192,3 +193,12 @@ class CreateSaver(Creator): return return data + + def get_instance_attr_defs(self): + return [ + BoolDef( + "review", + default=True, + label="Review" + ) + ] diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 4e5e151789..1e6d095cc2 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -3,25 +3,7 @@ import os import pyblish.api -def get_comp_render_range(comp): - """Return comp's start-end render range and global start-end range.""" - comp_attrs = comp.GetAttrs() - start = comp_attrs["COMPN_RenderStart"] - end = comp_attrs["COMPN_RenderEnd"] - global_start = comp_attrs["COMPN_GlobalStart"] - global_end = comp_attrs["COMPN_GlobalEnd"] - - # Whenever render ranges are undefined fall back - # to the comp's global start and end - if start == -1000000000: - start = global_start - if end == -1000000000: - end = global_end - - return start, end, global_start, global_end - - -class CollectInstances(pyblish.api.ContextPlugin): +class CollectInstanceData(pyblish.api.InstancePlugin): """Collect Fusion saver instances This additionally stores the Comp start and end render range in the @@ -33,59 +15,63 @@ class CollectInstances(pyblish.api.ContextPlugin): label = "Collect Instances Data" hosts = ["fusion"] - def process(self, context): + def process(self, instance): """Collect all image sequence tools""" - from openpype.hosts.fusion.api.lib import get_frame_path + context = instance.context - comp = context.data["currentComp"] - start, end, global_start, global_end = get_comp_render_range(comp) - context.data["frameStart"] = int(start) - context.data["frameEnd"] = int(end) - context.data["frameStartHandle"] = int(global_start) - context.data["frameEndHandle"] = int(global_end) + # Include creator attributes directly as instance data + creator_attributes = instance.data["creator_attributes"] + instance.data.update(creator_attributes) + + # Include start and end render frame in label + subset = instance.data["subset"] + start = context.data["frameStart"] + end = context.data["frameEnd"] + label = "{subset} ({start}-{end})".format(subset=subset, + start=int(start), + end=int(end)) + instance.data.update({ + "label": label, + + # todo: Allow custom frame range per instance + "frameStart": context.data["frameStart"], + "frameEnd": context.data["frameEnd"], + "frameStartHandle": context.data["frameStartHandle"], + "frameEndHandle": context.data["frameStartHandle"], + "fps": context.data["fps"], + }) + + # Add review family if the instance is marked as 'review' + # This could be done through a 'review' Creator attribute. + if instance.data.get("review", False): + self.log.info("Adding review family..") + instance.data["families"].append("review") + + if instance.data["family"] == "render": + # TODO: This should probably move into a collector of + # its own for the "render" family + from openpype.hosts.fusion.api.lib import get_frame_path + comp = context.data["currentComp"] + + # This is only the case for savers currently but not + # for workfile instances. So we assume saver here. + tool = instance.data["transientData"]["tool"] + path = tool["Clip"][comp.TIME_UNDEFINED] + + filename = os.path.basename(path) + head, padding, tail = get_frame_path(filename) + ext = os.path.splitext(path)[1] + assert tail == ext, ("Tail does not match %s" % ext) - for instance in context: - # Include start and end render frame in label - subset = instance.data["subset"] - label = "{subset} ({start}-{end})".format(subset=subset, - start=int(start), - end=int(end)) instance.data.update({ - "label": label, - # todo: Allow custom frame range per instance - "task": context.data["task"], - "frameStart": context.data["frameStart"], - "frameEnd": context.data["frameEnd"], - "frameStartHandle": context.data["frameStartHandle"], - "frameEndHandle": context.data["frameStartHandle"], - "fps": context.data["fps"], + "path": path, + "outputDir": os.path.dirname(path), + "ext": ext, # todo: should be redundant? + + # Backwards compatibility: embed tool in instance.data + "tool": tool }) - if instance.data["family"] == "render": - # TODO: This should probably move into a collector of - # its own for the "render" family - # This is only the case for savers currently but not - # for workfile instances. So we assume saver here. - tool = instance.data["transientData"]["tool"] - path = tool["Clip"][comp.TIME_UNDEFINED] - - filename = os.path.basename(path) - head, padding, tail = get_frame_path(filename) - ext = os.path.splitext(path)[1] - assert tail == ext, ("Tail does not match %s" % ext) - - instance.data.update({ - "path": path, - "outputDir": os.path.dirname(path), - "ext": ext, # todo: should be redundant? - - "families": ["render", "review"], - "family": "render", - - # Backwards compatibility: embed tool in instance.data - "tool": tool - }) - - # Add tool itself as member - instance.append(tool) + # Add tool itself as member + instance.append(tool) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 53d8eb64e1..30943edd4b 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -53,10 +53,8 @@ class Fusionlocal(pyblish.api.InstancePlugin): instance.data["representations"].append(repre) # review representation - repre_preview = repre.copy() - repre_preview["name"] = repre_preview["ext"] = "mp4" - repre_preview["tags"] = ["review", "ftrackreview", "delete"] - instance.data["representations"].append(repre_preview) + if instance.data.get("review", False): + repre["tags"] = ["review", "ftrackreview"] def render_once(self, context): """Render context comp only once, even with more render instances""" From c43a8b073296cb95ba1700faf5e32cb9f7c31fa4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 16:06:44 +0100 Subject: [PATCH 134/160] Collect comp frame range later in publishing - Otherwise it gets overridden by global plugin `CollectContextEntities` --- .../fusion/plugins/publish/collect_comp.py | 25 ----------- .../publish/collect_comp_frame_range.py | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 25 deletions(-) create mode 100644 openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp.py b/openpype/hosts/fusion/plugins/publish/collect_comp.py index 911071c9a0..d26bf66d1f 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp.py @@ -3,24 +3,6 @@ import pyblish.api from openpype.hosts.fusion.api import get_current_comp -def get_comp_render_range(comp): - """Return comp's start-end render range and global start-end range.""" - comp_attrs = comp.GetAttrs() - start = comp_attrs["COMPN_RenderStart"] - end = comp_attrs["COMPN_RenderEnd"] - global_start = comp_attrs["COMPN_GlobalStart"] - global_end = comp_attrs["COMPN_GlobalEnd"] - - # Whenever render ranges are undefined fall back - # to the comp's global start and end - if start == -1000000000: - start = global_start - if end == -1000000000: - end = global_end - - return start, end, global_start, global_end - - class CollectCurrentCompFusion(pyblish.api.ContextPlugin): """Collect current comp""" @@ -38,10 +20,3 @@ class CollectCurrentCompFusion(pyblish.api.ContextPlugin): # Store path to current file filepath = comp.GetAttrs().get("COMPS_FileName", "") context.data['currentFile'] = filepath - - # Store comp render ranges - start, end, global_start, global_end = get_comp_render_range(comp) - context.data["frameStart"] = int(start) - context.data["frameEnd"] = int(end) - context.data["frameStartHandle"] = int(global_start) - context.data["frameEndHandle"] = int(global_end) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py new file mode 100644 index 0000000000..dc88dd79c6 --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -0,0 +1,41 @@ +import pyblish.api + +from openpype.hosts.fusion.api import get_current_comp + + +def get_comp_render_range(comp): + """Return comp's start-end render range and global start-end range.""" + comp_attrs = comp.GetAttrs() + start = comp_attrs["COMPN_RenderStart"] + end = comp_attrs["COMPN_RenderEnd"] + global_start = comp_attrs["COMPN_GlobalStart"] + global_end = comp_attrs["COMPN_GlobalEnd"] + + # Whenever render ranges are undefined fall back + # to the comp's global start and end + if start == -1000000000: + start = global_start + if end == -1000000000: + end = global_end + + return start, end, global_start, global_end + + +class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): + """Collect current comp""" + + order = pyblish.api.CollectorOrder - 0.05 + label = "Collect Comp Frame Ranges" + hosts = ["fusion"] + + def process(self, context): + """Collect all image sequence tools""" + + comp = context.data["currentComp"] + + # Store comp render ranges + start, end, global_start, global_end = get_comp_render_range(comp) + context.data["frameStart"] = int(start) + context.data["frameEnd"] = int(end) + context.data["frameStartHandle"] = int(global_start) + context.data["frameEndHandle"] = int(global_end) From f6b8a8df61af591427e1192ecf7ce416db338db6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 16:07:54 +0100 Subject: [PATCH 135/160] Revert redundant variable name change since plugin is now basically reverted --- openpype/hosts/fusion/plugins/publish/collect_comp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp.py b/openpype/hosts/fusion/plugins/publish/collect_comp.py index d26bf66d1f..d1c49790fa 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp.py @@ -13,10 +13,10 @@ class CollectCurrentCompFusion(pyblish.api.ContextPlugin): def process(self, context): """Collect all image sequence tools""" - comp = get_current_comp() - assert comp, "Must have active Fusion composition" - context.data["currentComp"] = comp + current_comp = get_current_comp() + assert current_comp, "Must have active Fusion composition" + context.data["currentComp"] = current_comp # Store path to current file - filepath = comp.GetAttrs().get("COMPS_FileName", "") + filepath = current_comp.GetAttrs().get("COMPS_FileName", "") context.data['currentFile'] = filepath From 146f5cd439652d454b656163f00b1521bf5ee227 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 16:09:36 +0100 Subject: [PATCH 136/160] Add descriptive comment --- .../hosts/fusion/plugins/publish/collect_comp_frame_range.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index dc88dd79c6..98128e1ccf 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -24,6 +24,8 @@ def get_comp_render_range(comp): class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): """Collect current comp""" + # We run this after CollectorOrder - 0.1 otherwise it gets + # overridden by global plug-in `CollectContextEntities` order = pyblish.api.CollectorOrder - 0.05 label = "Collect Comp Frame Ranges" hosts = ["fusion"] From 43d084cf7f5a7f277a44c50bab623f31a74a8975 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 16:09:55 +0100 Subject: [PATCH 137/160] Remove unused import --- .../hosts/fusion/plugins/publish/collect_comp_frame_range.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index 98128e1ccf..c6d7a73a04 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -1,7 +1,5 @@ import pyblish.api -from openpype.hosts.fusion.api import get_current_comp - def get_comp_render_range(comp): """Return comp's start-end render range and global start-end range.""" From 1b18483f7b480665922847ceb556375d49026d35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Mar 2023 16:41:35 +0100 Subject: [PATCH 138/160] use right type for signal emit (#4584) --- openpype/tools/attribute_defs/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/attribute_defs/widgets.py b/openpype/tools/attribute_defs/widgets.py index 18e2e13d06..0d4e1e88a9 100644 --- a/openpype/tools/attribute_defs/widgets.py +++ b/openpype/tools/attribute_defs/widgets.py @@ -186,7 +186,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): class _BaseAttrDefWidget(QtWidgets.QWidget): # Type 'object' may not work with older PySide versions - value_changed = QtCore.Signal(object, uuid.UUID) + value_changed = QtCore.Signal(object, str) def __init__(self, attr_def, parent): super(_BaseAttrDefWidget, self).__init__(parent) From 70163a2f255413fbe706101b261dac3c3e65e2a8 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 7 Mar 2023 17:26:09 +0100 Subject: [PATCH 139/160] Added Create button to menu and set tab data for create and publish btn --- openpype/hosts/fusion/api/menu.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index 568e03464d..e37380017e 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -7,11 +7,11 @@ from openpype.style import load_stylesheet from openpype.lib import register_event_callback from openpype.hosts.fusion.scripts import ( set_rendermode, - duplicate_with_inputs + duplicate_with_inputs, ) from openpype.hosts.fusion.api.lib import ( set_asset_framerange, - set_asset_resolution + set_asset_resolution, ) from openpype.pipeline import legacy_io from openpype.resources import get_openpype_icon_filepath @@ -45,14 +45,17 @@ class OpenPypeMenu(QtWidgets.QWidget): self.setWindowTitle("OpenPype") asset_label = QtWidgets.QLabel("Context", self) - asset_label.setStyleSheet("""QLabel { + asset_label.setStyleSheet( + """QLabel { font-size: 14px; font-weight: 600; color: #5f9fb8; - }""") + }""" + ) asset_label.setAlignment(QtCore.Qt.AlignHCenter) workfiles_btn = QtWidgets.QPushButton("Workfiles...", self) + create_btn = QtWidgets.QPushButton("Create...", self) publish_btn = QtWidgets.QPushButton("Publish...", self) load_btn = QtWidgets.QPushButton("Load...", self) manager_btn = QtWidgets.QPushButton("Manage...", self) @@ -76,6 +79,7 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addSpacing(20) layout.addWidget(load_btn) + layout.addWidget(create_btn) layout.addWidget(publish_btn) layout.addWidget(manager_btn) @@ -99,13 +103,15 @@ class OpenPypeMenu(QtWidgets.QWidget): self.asset_label = asset_label workfiles_btn.clicked.connect(self.on_workfile_clicked) + create_btn.clicked.connect(self.on_create_clicked) publish_btn.clicked.connect(self.on_publish_clicked) load_btn.clicked.connect(self.on_load_clicked) manager_btn.clicked.connect(self.on_manager_clicked) libload_btn.clicked.connect(self.on_libload_clicked) rendermode_btn.clicked.connect(self.on_rendermode_clicked) duplicate_with_inputs_btn.clicked.connect( - self.on_duplicate_with_inputs_clicked) + self.on_duplicate_with_inputs_clicked + ) set_resolution_btn.clicked.connect(self.on_set_resolution_clicked) set_framerange_btn.clicked.connect(self.on_set_framerange_clicked) @@ -127,7 +133,6 @@ class OpenPypeMenu(QtWidgets.QWidget): self.asset_label.setText(label) def register_callback(self, name, fn): - # Create a wrapper callback that we only store # for as long as we want it to persist as callback def _callback(*args): @@ -142,8 +147,11 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_workfile_clicked(self): host_tools.show_workfiles() + def on_create_clicked(self): + host_tools.show_publisher(tab="create") + def on_publish_clicked(self): - host_tools.show_publisher() + host_tools.show_publisher(tab="publish") def on_load_clicked(self): host_tools.show_loader(use_context=True) From b1fac42e94a668e9b072dc5f64edca865d06afcd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 7 Mar 2023 17:26:31 +0100 Subject: [PATCH 140/160] updating pr tempate --- .github/pull_request_template.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 20ae298f70..2adaffd23d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,16 +1,9 @@ -## Brief description -First sentence is brief description. - -## Description -Next paragraf is more elaborate text with more info. This will be displayed for example in collapsed form under the first sentence in a changelog. +## Changelog Description +Paragraphs contain detailed information on the changes made to the product or service, providing an in-depth description of the updates and enhancements. They can be used to explain the reasoning behind the changes, or to highlight the importance of the new features. Paragraphs can often include links to further information or support documentation. ## Additional info -The rest will be ignored in changelog and should contain any additional -technical information. - -## Documentation (add _"type: documentation"_ label) -[feature_documentation](future_url_after_it_will_be_merged) +Paragraphs of text giving context of additional technical information or code examples. ## Testing notes: 1. start with this step -2. follow this step \ No newline at end of file +2. follow this step From 9c9c134a794a5ab9bd36a415ef15d2b2b64dbdd8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 17:39:05 +0100 Subject: [PATCH 141/160] Use passthrough state of saver tool to store and load the active state --- openpype/hosts/fusion/plugins/create/create_saver.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index bf11dc95c5..e581bac20f 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -109,6 +109,12 @@ class CreateSaver(Creator): def _imprint(self, tool, data): # Save all data in a "openpype.{key}" = value data + + active = data.pop("active", None) + if active is not None: + # Use active value to set the passthrough state + tool.SetAttrs({"TOOLB_PassThrough": not active}) + for key, value in data.items(): tool.SetData(f"openpype.{key}", value) @@ -192,6 +198,11 @@ class CreateSaver(Creator): if key not in data or data[key] != value: return + # Get active state from the actual tool state + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + data["active"] = not passthrough + return data def get_instance_attr_defs(self): From 5c8bbe28713aaabbabf99b4739f1654a0bea4b71 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 17:41:03 +0100 Subject: [PATCH 142/160] Remove pyblish callback which does nothing in new publisher --- openpype/hosts/fusion/api/pipeline.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/openpype/hosts/fusion/api/pipeline.py b/openpype/hosts/fusion/api/pipeline.py index b982e1c2e9..a768a3f0f8 100644 --- a/openpype/hosts/fusion/api/pipeline.py +++ b/openpype/hosts/fusion/api/pipeline.py @@ -102,9 +102,6 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_creator_plugin_path(CREATE_PATH) register_inventory_action_path(INVENTORY_PATH) - pyblish.api.register_callback( - "instanceToggled", on_pyblish_instance_toggled) - # Register events register_event_callback("open", on_after_open) register_event_callback("save", on_save) @@ -163,29 +160,6 @@ class FusionHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return comp.GetData("openpype") or {} -def on_pyblish_instance_toggled(instance, old_value, new_value): - """Toggle saver tool passthrough states on instance toggles.""" - comp = instance.context.data.get("currentComp") - if not comp: - return - - savers = [tool for tool in instance if - getattr(tool, "ID", None) == "Saver"] - if not savers: - return - - # Whether instances should be passthrough based on new value - passthrough = not new_value - with comp_lock_and_undo_chunk(comp, - undo_queue_name="Change instance " - "active state"): - for tool in savers: - attrs = tool.GetAttrs() - current = attrs["TOOLB_PassThrough"] - if current != passthrough: - tool.SetAttrs({"TOOLB_PassThrough": passthrough}) - - def on_new(event): comp = event["Rets"]["comp"] validate_comp_prefs(comp, force_repair=True) From 0f037666f83226e9ec832a58a49ee848683b9b04 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 17:49:31 +0100 Subject: [PATCH 143/160] Change menu order to how it was originally and match with e.g. maya menu order --- openpype/hosts/fusion/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/menu.py b/openpype/hosts/fusion/api/menu.py index e37380017e..343f5f803a 100644 --- a/openpype/hosts/fusion/api/menu.py +++ b/openpype/hosts/fusion/api/menu.py @@ -56,8 +56,8 @@ class OpenPypeMenu(QtWidgets.QWidget): workfiles_btn = QtWidgets.QPushButton("Workfiles...", self) create_btn = QtWidgets.QPushButton("Create...", self) - publish_btn = QtWidgets.QPushButton("Publish...", self) load_btn = QtWidgets.QPushButton("Load...", self) + publish_btn = QtWidgets.QPushButton("Publish...", self) manager_btn = QtWidgets.QPushButton("Manage...", self) libload_btn = QtWidgets.QPushButton("Library...", self) rendermode_btn = QtWidgets.QPushButton("Set render mode...", self) @@ -78,8 +78,8 @@ class OpenPypeMenu(QtWidgets.QWidget): layout.addSpacing(20) - layout.addWidget(load_btn) layout.addWidget(create_btn) + layout.addWidget(load_btn) layout.addWidget(publish_btn) layout.addWidget(manager_btn) From acbfb5985b52516ac99d45bd6b5e7f8121d89b6c Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Mon, 6 Mar 2023 14:50:00 +0100 Subject: [PATCH 144/160] Fixed task itteration From the last PR (https://github.com/ynput/OpenPype/pull/4425) a comment-commit last second messed up and resultet in two lines being the same, crashing the script. This fixes that. --- .../modules/kitsu/utils/update_op_with_zou.py | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 053e803ff3..4fa8cf9fdd 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -95,7 +95,8 @@ def update_op_assets( op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) item_doc = get_asset_by_id( - project_name, insert_result.inserted_id) + project_name, insert_result.inserted_id + ) # Update asset item_data = deepcopy(item_doc["data"]) @@ -133,39 +134,47 @@ def update_op_assets( try: fps = float(item_data.get("fps")) except (TypeError, ValueError): - fps = float(gazu_project.get( - "fps", project_doc["data"].get("fps", 25))) + fps = float( + gazu_project.get("fps", project_doc["data"].get("fps", 25)) + ) item_data["fps"] = fps # Resolution, fall back to project default match_res = re.match( r"(\d+)x(\d+)", - item_data.get("resolution", gazu_project.get("resolution")) + item_data.get("resolution", gazu_project.get("resolution")), ) if match_res: item_data["resolutionWidth"] = int(match_res.group(1)) item_data["resolutionHeight"] = int(match_res.group(2)) else: item_data["resolutionWidth"] = project_doc["data"].get( - "resolutionWidth") + "resolutionWidth" + ) item_data["resolutionHeight"] = project_doc["data"].get( - "resolutionHeight") + "resolutionHeight" + ) # Properties that doesn't fully exist in Kitsu. # Guessing those property names below: # Pixel Aspect Ratio item_data["pixelAspect"] = item_data.get( - "pixel_aspect", project_doc["data"].get("pixelAspect")) + "pixel_aspect", project_doc["data"].get("pixelAspect") + ) # Handle Start item_data["handleStart"] = item_data.get( - "handle_start", project_doc["data"].get("handleStart")) + "handle_start", project_doc["data"].get("handleStart") + ) # Handle End item_data["handleEnd"] = item_data.get( - "handle_end", project_doc["data"].get("handleEnd")) + "handle_end", project_doc["data"].get("handleEnd") + ) # Clip In item_data["clipIn"] = item_data.get( - "clip_in", project_doc["data"].get("clipIn")) + "clip_in", project_doc["data"].get("clipIn") + ) # Clip Out item_data["clipOut"] = item_data.get( - "clip_out", project_doc["data"].get("clipOut")) + "clip_out", project_doc["data"].get("clipOut") + ) # Tasks tasks_list = [] @@ -175,11 +184,9 @@ def update_op_assets( elif item_type == "Shot": tasks_list = gazu.task.all_tasks_for_shot(item) item_data["tasks"] = { - item_data["tasks"] = { - t["task_type_name"]: { - "type": t["task_type_name"], - "zou": gazu.task.get_task(t["id"]), - } + t["task_type_name"]: { + "type": t["task_type_name"], + "zou": gazu.task.get_task(t["id"]), } for t in tasks_list } @@ -218,7 +225,9 @@ def update_op_assets( if parent_zou_id_dict is not None: visual_parent_doc_id = ( parent_zou_id_dict.get("_id") - if parent_zou_id_dict else None) + if parent_zou_id_dict + else None + ) if visual_parent_doc_id is None: # Find root folder doc ("Assets" or "Shots") @@ -345,7 +354,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None): + login: str, password: str, ignore_projects: list = None +): """Update all OP projects in DB with Zou data. Args: @@ -390,7 +400,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): if not project: project = gazu.project.get_project_by_name(project["name"]) - log.info("Synchronizing {}...".format(project['name'])) + log.info("Synchronizing {}...".format(project["name"])) # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) @@ -473,8 +483,11 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, project, project_dict, - all_entities, zou_ids_and_asset_docs + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, ) ] ) From 40125fa6a5518b7ca202a892a3f4196913058600 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 10:17:44 +0100 Subject: [PATCH 145/160] Avoid error in PySide6+ --- openpype/widgets/popup.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openpype/widgets/popup.py b/openpype/widgets/popup.py index 97a8461060..28bbd45072 100644 --- a/openpype/widgets/popup.py +++ b/openpype/widgets/popup.py @@ -98,15 +98,22 @@ class Popup(QtWidgets.QDialog): height = window.height() height = max(height, window.sizeHint().height()) - desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() - screen_geometry = window.geometry() + try: + screen = QtWidgets.QApplication.primaryScreen() + desktop_geometry = screen.availableGeometry() + except AttributeError: + # Backwards compatibility for older Qt versions + # PySide6 removed QDesktopWidget + desktop_geometry = QtWidgets.QDesktopWidget().availableGeometry() - screen_width = screen_geometry.width() - screen_height = screen_geometry.height() + window_geometry = window.geometry() + + screen_width = window_geometry.width() + screen_height = window_geometry.height() # Calculate width and height of system tray - systray_width = screen_geometry.width() - desktop_geometry.width() - systray_height = screen_geometry.height() - desktop_geometry.height() + systray_width = window_geometry.width() - desktop_geometry.width() + systray_height = window_geometry.height() - desktop_geometry.height() padding = 10 From 300a4435101e8ae6608ef4024f002606f9f867d5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 11:08:10 +0100 Subject: [PATCH 146/160] Use screen of window instead of primary screen Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/widgets/popup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/widgets/popup.py b/openpype/widgets/popup.py index 28bbd45072..225c5e18a1 100644 --- a/openpype/widgets/popup.py +++ b/openpype/widgets/popup.py @@ -99,7 +99,7 @@ class Popup(QtWidgets.QDialog): height = max(height, window.sizeHint().height()) try: - screen = QtWidgets.QApplication.primaryScreen() + screen = window.screen() desktop_geometry = screen.availableGeometry() except AttributeError: # Backwards compatibility for older Qt versions From ecfea3dee2be05318ec9cfb88802f357fdb2c0e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Mar 2023 20:42:49 +0100 Subject: [PATCH 147/160] Explicitly set the `handleStart` and `handleEnd` otherwise other global plug-ins will force in other data like asset data. --- .../hosts/fusion/plugins/publish/collect_comp_frame_range.py | 2 ++ openpype/hosts/fusion/plugins/publish/collect_instances.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py index c6d7a73a04..fbd7606cd7 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/collect_comp_frame_range.py @@ -39,3 +39,5 @@ class CollectFusionCompFrameRanges(pyblish.api.ContextPlugin): context.data["frameEnd"] = int(end) context.data["frameStartHandle"] = int(global_start) context.data["frameEndHandle"] = int(global_end) + context.data["handleStart"] = int(start) - int(global_start) + context.data["handleEnd"] = int(global_end) - int(end) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 1e6d095cc2..af227f03db 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -39,6 +39,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): "frameEnd": context.data["frameEnd"], "frameStartHandle": context.data["frameStartHandle"], "frameEndHandle": context.data["frameStartHandle"], + "handleStart": context.data["handleStart"], + "handleEnd": context.data["handleEnd"], "fps": context.data["fps"], }) From 5088edfdade4bcb8d096ef45502ba0cf235fb6a5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 8 Mar 2023 03:30:35 +0000 Subject: [PATCH 148/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index 2939ddbbac..c7a5e9bea5 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.2-nightly.4" +__version__ = "3.15.2-nightly.5" From fb0f39b3ccff52ac2df9e2dfe0cbb743bfb3ac92 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov <11698866+movalex@users.noreply.github.com> Date: Wed, 8 Mar 2023 12:38:23 +0300 Subject: [PATCH 149/160] add up to 3 decimals precision to the frame rate settings (#4571) * add up to 3 decimals to fps allows input 23.976 to the FPS settings both in Project Manager and the Project Anatomy. * set fps and pixel aspect precision steps default values --- .../schemas/schema_anatomy_attributes.json | 2 +- .../project_manager/project_manager/delegates.py | 5 ++++- .../tools/project_manager/project_manager/view.py | 14 ++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json index 3667c9d5d8..a728024376 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_anatomy_attributes.json @@ -10,7 +10,7 @@ "type": "number", "key": "fps", "label": "Frame Rate", - "decimal": 2, + "decimal": 3, "minimum": 0 }, { diff --git a/openpype/tools/project_manager/project_manager/delegates.py b/openpype/tools/project_manager/project_manager/delegates.py index 79e9554b0f..023dd668ec 100644 --- a/openpype/tools/project_manager/project_manager/delegates.py +++ b/openpype/tools/project_manager/project_manager/delegates.py @@ -83,15 +83,18 @@ class NumberDelegate(QtWidgets.QStyledItemDelegate): decimals(int): How many decimal points can be used. Float will be used as value if is higher than 0. """ - def __init__(self, minimum, maximum, decimals, *args, **kwargs): + def __init__(self, minimum, maximum, decimals, step, *args, **kwargs): super(NumberDelegate, self).__init__(*args, **kwargs) self.minimum = minimum self.maximum = maximum self.decimals = decimals + self.step = step def createEditor(self, parent, option, index): if self.decimals > 0: editor = DoubleSpinBoxScrollFixed(parent) + editor.setSingleStep(self.step) + editor.setDecimals(self.decimals) else: editor = SpinBoxScrollFixed(parent) diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index fa08943ea5..b35491c5b2 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -26,10 +26,11 @@ class NameDef: class NumberDef: - def __init__(self, minimum=None, maximum=None, decimals=None): + def __init__(self, minimum=None, maximum=None, decimals=None, step=None): self.minimum = 0 if minimum is None else minimum self.maximum = 999999999 if maximum is None else maximum self.decimals = 0 if decimals is None else decimals + self.step = 1 if decimals is None else step class TypeDef: @@ -73,14 +74,14 @@ class HierarchyView(QtWidgets.QTreeView): "type": TypeDef(), "frameStart": NumberDef(1), "frameEnd": NumberDef(1), - "fps": NumberDef(1, decimals=2), + "fps": NumberDef(1, decimals=3, step=1), "resolutionWidth": NumberDef(0), "resolutionHeight": NumberDef(0), "handleStart": NumberDef(0), "handleEnd": NumberDef(0), "clipIn": NumberDef(1), "clipOut": NumberDef(1), - "pixelAspect": NumberDef(0, decimals=2), + "pixelAspect": NumberDef(0, decimals=2, step=0.01), "tools_env": ToolsDef() } @@ -96,6 +97,10 @@ class HierarchyView(QtWidgets.QTreeView): "stretch": QtWidgets.QHeaderView.Interactive, "width": 140 }, + "fps": { + "stretch": QtWidgets.QHeaderView.Interactive, + "width": 65 + }, "tools_env": { "stretch": QtWidgets.QHeaderView.Interactive, "width": 200 @@ -148,7 +153,8 @@ class HierarchyView(QtWidgets.QTreeView): delegate = NumberDelegate( item_type.minimum, item_type.maximum, - item_type.decimals + item_type.decimals, + item_type.step ) elif isinstance(item_type, TypeDef): From 0f45af2d36f5a29f550d412c0d346154d0f855c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 6 Mar 2023 15:10:37 +0100 Subject: [PATCH 150/160] use 'get_representations' instead of 'legacy_io' query --- .../hosts/unreal/plugins/load/load_layout.py | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index c1d66ddf2a..18653e81cb 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Loader for layouts.""" import json +import collections from pathlib import Path import unreal @@ -12,9 +13,7 @@ from unreal import FBXImportType from unreal import MovieSceneLevelVisibilityTrack from unreal import MovieSceneSubTrack -from bson.objectid import ObjectId - -from openpype.client import get_asset_by_name, get_assets +from openpype.client import get_asset_by_name, get_assets, get_representations from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -410,6 +409,29 @@ class LayoutLoader(plugin.Loader): return sequence, (min_frame, max_frame) + def _get_repre_docs_by_version_id(self, project_name, data): + version_ids = { + element.get("version") + for element in data + if element.get("representation") + } + version_ids.discard(None) + + output = collections.defaultdict(list) + if not version_ids: + return output + + repre_docs = get_representations( + project_name, + representation_names=["fbx", "abc"], + version_ids=version_ids, + fields=["_id", "parent", "name"] + ) + for repre_doc in repre_docs: + version_id = str(repre_doc["parent"]) + output[version_id].append(repre_doc) + return output + def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -429,31 +451,21 @@ class LayoutLoader(plugin.Loader): loaded_assets = [] + repre_docs_by_version_id = self._get_repre_docs_by_version_id(data) for element in data: representation = None repr_format = None if element.get('representation'): - # representation = element.get('representation') - - self.log.info(element.get("version")) - - valid_formats = ['fbx', 'abc'] - - repr_data = legacy_io.find_one({ - "type": "representation", - "parent": ObjectId(element.get("version")), - "name": {"$in": valid_formats} - }) - repr_format = repr_data.get('name') - - if not repr_data: + repre_docs = repre_docs_by_version_id[element.get("version")] + if not repre_docs: self.log.error( f"No valid representation found for version " f"{element.get('version')}") continue + repre_doc = repre_docs[0] + representation = str(repre_doc["_id"]) + repr_format = repre_doc["name"] - representation = str(repr_data.get('_id')) - print(representation) # This is to keep compatibility with old versions of the # json format. elif element.get('reference_fbx'): From 47b1daf0f5ef0cf11b89026c8b342b18782e1956 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Mar 2023 11:21:38 +0100 Subject: [PATCH 151/160] get project name other way --- openpype/hosts/unreal/plugins/load/load_layout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 18653e81cb..63d415a52b 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -409,7 +409,7 @@ class LayoutLoader(plugin.Loader): return sequence, (min_frame, max_frame) - def _get_repre_docs_by_version_id(self, project_name, data): + def _get_repre_docs_by_version_id(self, data): version_ids = { element.get("version") for element in data @@ -421,6 +421,7 @@ class LayoutLoader(plugin.Loader): if not version_ids: return output + project_name = legacy_io.active_project() repre_docs = get_representations( project_name, representation_names=["fbx", "abc"], From c58778194f31b37235b201d9f0132cf97909aa77 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 8 Mar 2023 16:30:30 +0100 Subject: [PATCH 152/160] Implementation of a new splash screen --- .../unreal/hooks/pre_workfile_preparation.py | 90 ++++- .../OpenPype/Private/AssetContainer.cpp | 6 +- openpype/hosts/unreal/lib.py | 22 +- openpype/hosts/unreal/ue_workers.py | 338 ++++++++++++++++++ openpype/widgets/README.md | 102 ++++++ openpype/widgets/splash_screen.py | 253 +++++++++++++ 6 files changed, 793 insertions(+), 18 deletions(-) create mode 100644 openpype/hosts/unreal/ue_workers.py create mode 100644 openpype/widgets/README.md create mode 100644 openpype/widgets/splash_screen.py diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 4c9f8258f5..8ede80f7fd 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -3,7 +3,11 @@ import os import copy from pathlib import Path +from openpype.widgets.splash_screen import SplashScreen +from qtpy import QtCore +from openpype.hosts.unreal.ue_workers import UEProjectGenerationWorker, UEPluginInstallWorker +from openpype import resources from openpype.lib import ( PreLaunchHook, ApplicationLaunchFailed, @@ -22,6 +26,7 @@ class UnrealPrelaunchHook(PreLaunchHook): shell script. """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -58,6 +63,70 @@ class UnrealPrelaunchHook(PreLaunchHook): # Return filename return filled_anatomy[workfile_template_key]["file"] + def exec_plugin_install(self, engine_path: Path, env: dict = None): + # set up the QThread and worker with necessary signals + env = env or os.environ + q_thread = QtCore.QThread() + ue_plugin_worker = UEPluginInstallWorker() + + q_thread.started.connect(ue_plugin_worker.run) + ue_plugin_worker.setup(engine_path, env) + ue_plugin_worker.moveToThread(q_thread) + + splash_screen = SplashScreen("Installing plugin", + resources.get_resource("app_icons", "ue4.png")) + + # set up the splash screen with necessary triggers + ue_plugin_worker.installing.connect(splash_screen.update_top_label_text) + ue_plugin_worker.progress.connect(splash_screen.update_progress) + ue_plugin_worker.log.connect(splash_screen.append_log) + ue_plugin_worker.finished.connect(splash_screen.quit_and_close) + ue_plugin_worker.failed.connect(splash_screen.fail) + + splash_screen.start_thread(q_thread) + splash_screen.show_ui() + + if not splash_screen.was_proc_successful(): + raise ApplicationLaunchFailed("Couldn't run the application! " + "Plugin failed to install!") + + def exec_ue_project_gen(self, + engine_version: str, + unreal_project_name: str, + engine_path: Path, + project_dir: Path): + self.log.info(( + f"{self.signature} Creating unreal " + f"project [ {unreal_project_name} ]" + )) + + q_thread = QtCore.QThread() + ue_project_worker = UEProjectGenerationWorker() + ue_project_worker.setup( + engine_version, + unreal_project_name, + engine_path, + project_dir + ) + ue_project_worker.moveToThread(q_thread) + q_thread.started.connect(ue_project_worker.run) + + splash_screen = SplashScreen("Initializing UE project", + resources.get_resource("app_icons", "ue4.png")) + + ue_project_worker.stage_begin.connect(splash_screen.update_top_label_text) + ue_project_worker.progress.connect(splash_screen.update_progress) + ue_project_worker.log.connect(splash_screen.append_log) + ue_project_worker.finished.connect(splash_screen.quit_and_close) + ue_project_worker.failed.connect(splash_screen.fail) + + splash_screen.start_thread(q_thread) + splash_screen.show_ui() + + if not splash_screen.was_proc_successful(): + raise ApplicationLaunchFailed("Couldn't run the application! " + "Failed to generate the project!") + def execute(self): """Hook entry method.""" workdir = self.launch_context.env["AVALON_WORKDIR"] @@ -137,23 +206,18 @@ class UnrealPrelaunchHook(PreLaunchHook): if self.launch_context.env.get(env_key): os.environ[env_key] = self.launch_context.env[env_key] - engine_path = detected[engine_version] + engine_path: Path = Path(detected[engine_version]) - unreal_lib.try_installing_plugin(Path(engine_path), os.environ) + if not unreal_lib.check_plugin_existence(engine_path): + self.exec_plugin_install(engine_path) project_file = project_path / unreal_project_filename - if not project_file.is_file(): - self.log.info(( - f"{self.signature} creating unreal " - f"project [ {unreal_project_name} ]" - )) - unreal_lib.create_unreal_project( - unreal_project_name, - engine_version, - project_path, - engine_path=Path(engine_path) - ) + if not project_file.is_file(): + self.exec_ue_project_gen(engine_version, + unreal_project_name, + engine_path, + project_path) self.launch_context.env["OPENPYPE_UNREAL_VERSION"] = engine_version # Append project file to launch arguments diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp index 0bea9e3d78..06dcd67808 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/AssetContainer.cpp @@ -30,7 +30,7 @@ void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); + FString assetFName = AssetData.ObjectPath.ToString(); UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -60,7 +60,7 @@ void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); + FString assetFName = AssetData.ObjectPath.ToString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); @@ -93,7 +93,7 @@ void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& // get asset path and class FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); + FString assetFName = AssetData.ObjectPath.ToString(); // split path assetPath.ParseIntoArray(split, TEXT(" "), true); diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 28a5106042..08cc57a6cd 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -365,6 +365,26 @@ def _get_build_id(engine_path: Path, ue_version: str) -> str: return "{" + loaded_modules.get("BuildId") + "}" +def check_plugin_existence(engine_path: Path, env: dict = None) -> bool: + env = env or os.environ + integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(integration_plugin_path): + raise RuntimeError("Path to the integration plugin is null!") + + # Create a path to the plugin in the engine + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + + if not op_plugin_path.is_dir(): + return False + + if not (op_plugin_path / "Binaries").is_dir() \ + or not (op_plugin_path / "Intermediate").is_dir(): + return False + + return True + + def try_installing_plugin(engine_path: Path, env: dict = None) -> None: env = env or os.environ @@ -377,7 +397,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None: op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" if not op_plugin_path.is_dir(): - print("--- OpenPype Plugin is not present. Installing ...") op_plugin_path.mkdir(parents=True, exist_ok=True) engine_plugin_config_path: Path = op_plugin_path / "Config" @@ -387,7 +406,6 @@ def try_installing_plugin(engine_path: Path, env: dict = None) -> None: if not (op_plugin_path / "Binaries").is_dir() \ or not (op_plugin_path / "Intermediate").is_dir(): - print("--- Binaries are not present. Building the plugin ...") _build_and_move_plugin(engine_path, op_plugin_path, env) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py new file mode 100644 index 0000000000..35735fcaa1 --- /dev/null +++ b/openpype/hosts/unreal/ue_workers.py @@ -0,0 +1,338 @@ +import json +import os +import platform +import re +import subprocess +from distutils import dir_util +from pathlib import Path +from typing import List + +import openpype.hosts.unreal.lib as ue_lib + +from qtpy import QtCore + + +def parse_comp_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: + match = re.search('\[[1-9]+/[0-9]+\]', line) + if match is not None: + split: list[str] = match.group().split('/') + curr: float = float(split[0][1:]) + total: float = float(split[1][:-1]) + progress_signal.emit(int((curr / total) * 100.0)) + + +def parse_prj_progress(line: str, progress_signal: QtCore.Signal(int)) -> int: + match = re.search('@progress', line) + if match is not None: + percent_match = re.search('\d{1,3}', line) + progress_signal.emit(int(percent_match.group())) + + +class UEProjectGenerationWorker(QtCore.QObject): + finished = QtCore.Signal(str) + failed = QtCore.Signal(str) + progress = QtCore.Signal(int) + log = QtCore.Signal(str) + stage_begin = QtCore.Signal(str) + + ue_version: str = None + project_name: str = None + env = None + engine_path: Path = None + project_dir: Path = None + dev_mode = False + + def setup(self, ue_version: str, + project_name, + engine_path: Path, + project_dir: Path, + dev_mode: bool = False, + env: dict = None): + + self.ue_version = ue_version + self.project_dir = project_dir + self.env = env or os.environ + + preset = ue_lib.get_project_settings( + project_name + )["unreal"]["project_setup"] + + if dev_mode or preset["dev_mode"]: + self.dev_mode = True + + self.project_name = project_name + self.engine_path = engine_path + + def run(self): + + + ue_id = ".".join(self.ue_version.split(".")[:2]) + # get unreal engine identifier + # ------------------------------------------------------------------------- + # FIXME (antirotor): As of 4.26 this is problem with UE4 built from + # sources. In that case Engine ID is calculated per machine/user and not + # from Engine files as this code then reads. This then prevents UE4 + # to directly open project as it will complain about project being + # created in different UE4 version. When user convert such project + # to his UE4 version, Engine ID is replaced in uproject file. If some + # other user tries to open it, it will present him with similar error. + + # engine_path should be the location of UE_X.X folder + + ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path, + self.ue_version) + cmdlet_project = ue_lib.get_path_to_cmdlet_project(self.ue_version) + project_file = self.project_dir / f"{self.project_name}.uproject" + + print("--- Generating a new project ...") + # 1st stage + stage_count = 2 + if self.dev_mode: + stage_count = 4 + + self.stage_begin.emit(f'Generating a new UE project ... 1 out of ' + f'{stage_count}') + + commandlet_cmd = [f'{ue_editor_exe.as_posix()}', + f'{cmdlet_project.as_posix()}', + f'-run=OPGenerateProject', + f'{project_file.resolve().as_posix()}'] + + if self.dev_mode: + commandlet_cmd.append('-GenerateCode') + + gen_process = subprocess.Popen(commandlet_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + + for line in gen_process.stdout: + decoded_line = line.decode(errors="replace") + print(decoded_line, end='') + self.log.emit(decoded_line) + gen_process.stdout.close() + return_code = gen_process.wait() + + if return_code and return_code != 0: + msg = 'Failed to generate ' + self.project_name \ + + f' project! Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + print("--- Project has been generated successfully.") + self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1 out' + f' of {stage_count}') + + with open(project_file.as_posix(), mode="r+") as pf: + pf_json = json.load(pf) + pf_json["EngineAssociation"] = ue_lib.get_build_id(self.engine_path, + self.ue_version) + pf.seek(0) + json.dump(pf_json, pf, indent=4) + pf.truncate() + print(f'--- Engine ID has been written into the project file') + + self.progress.emit(90) + if self.dev_mode: + # 2nd stage + self.stage_begin.emit(f'Generating project files ... 2 out of ' + f'{stage_count}') + + self.progress.emit(0) + ubt_path = ue_lib.get_path_to_ubt(self.engine_path, self.ue_version) + + arch = "Win64" + if platform.system().lower() == "windows": + arch = "Win64" + elif platform.system().lower() == "linux": + arch = "Linux" + elif platform.system().lower() == "darwin": + # we need to test this out + arch = "Mac" + + gen_prj_files_cmd = [ubt_path.as_posix(), + "-projectfiles", + f"-project={project_file}", + "-progress"] + gen_proc = subprocess.Popen(gen_prj_files_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + for line in gen_proc.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + parse_prj_progress(decoded_line, self.progress) + + gen_proc.stdout.close() + return_code = gen_proc.wait() + + if return_code and return_code != 0: + msg = 'Failed to generate project files! ' \ + f'Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + self.stage_begin.emit(f'Building the project ... 3 out of ' + f'{stage_count}') + self.progress.emit(0) + # 3rd stage + build_prj_cmd = [ubt_path.as_posix(), + f"-ModuleWithSuffix={self.project_name},3555", + arch, + "Development", + "-TargetType=Editor", + f'-Project={project_file}', + f'{project_file}', + "-IgnoreJunk"] + + build_prj_proc = subprocess.Popen(build_prj_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + for line in build_prj_proc.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + parse_comp_progress(decoded_line, self.progress) + + build_prj_proc.stdout.close() + return_code = build_prj_proc.wait() + + if return_code and return_code != 0: + msg = 'Failed to build project! ' \ + f'Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + # ensure we have PySide2 installed in engine + + self.progress.emit(0) + self.stage_begin.emit(f'Checking PySide2 installation... {stage_count} ' + f'out of {stage_count}') + python_path = None + if platform.system().lower() == "windows": + python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Win64/python.exe") + + if platform.system().lower() == "linux": + python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Linux/bin/python3") + + if platform.system().lower() == "darwin": + python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" + "Python3/Mac/bin/python3") + + if not python_path: + msg = "Unsupported platform" + self.failed.emit(msg, 1) + raise NotImplementedError(msg) + if not python_path.exists(): + msg = f"Unreal Python not found at {python_path}" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + subprocess.check_call( + [python_path.as_posix(), "-m", "pip", "install", "pyside2"] + ) + self.progress.emit(100) + self.finished.emit("Project successfully built!") + + +class UEPluginInstallWorker(QtCore.QObject): + finished = QtCore.Signal(str) + installing = QtCore.Signal(str) + failed = QtCore.Signal(str, int) + progress = QtCore.Signal(int) + log = QtCore.Signal(str) + + engine_path: Path = None + env = None + + def setup(self, engine_path: Path, env: dict = None, ): + self.engine_path = engine_path + self.env = env or os.environ + + def _build_and_move_plugin(self, plugin_build_path: Path): + uat_path: Path = ue_lib.get_path_to_uat(self.engine_path) + src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(src_plugin_dir): + msg = "Path to the integration plugin is null!" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + + if not uat_path.is_file(): + msg = "Building failed! Path to UAT is invalid!" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + + temp_dir: Path = src_plugin_dir.parent / "Temp" + temp_dir.mkdir(exist_ok=True) + uplugin_path: Path = src_plugin_dir / "OpenPype.uplugin" + + # in order to successfully build the plugin, + # It must be built outside the Engine directory and then moved + build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', + 'BuildPlugin', + f'-Plugin={uplugin_path.as_posix()}', + f'-Package={temp_dir.as_posix()}'] + + build_proc = subprocess.Popen(build_plugin_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + for line in build_proc.stdout: + decoded_line: str = line.decode(errors='replace') + print(decoded_line, end='') + self.log.emit(decoded_line) + parse_comp_progress(decoded_line, self.progress) + + build_proc.stdout.close() + return_code = build_proc.wait() + + if return_code and return_code != 0: + msg = 'Failed to build plugin' \ + f' project! Exited with return code {return_code}' + self.failed.emit(msg, return_code) + raise RuntimeError(msg) + + # Copy the contents of the 'Temp' dir into the + # 'OpenPype' directory in the engine + dir_util.copy_tree(temp_dir.as_posix(), + plugin_build_path.as_posix()) + + # We need to also copy the config folder. + # The UAT doesn't include the Config folder in the build + plugin_install_config_path: Path = plugin_build_path / "Config" + src_plugin_config_path = src_plugin_dir / "Config" + + dir_util.copy_tree(src_plugin_config_path.as_posix(), + plugin_install_config_path.as_posix()) + + dir_util.remove_tree(temp_dir.as_posix()) + + def run(self): + src_plugin_dir = Path(self.env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(src_plugin_dir): + msg = "Path to the integration plugin is null!" + self.failed.emit(msg, 1) + raise RuntimeError(msg) + + # Create a path to the plugin in the engine + op_plugin_path = self.engine_path / \ + "Engine/Plugins/Marketplace/OpenPype" + + if not op_plugin_path.is_dir(): + self.installing.emit("Installing and building the plugin ...") + op_plugin_path.mkdir(parents=True, exist_ok=True) + + engine_plugin_config_path = op_plugin_path / "Config" + engine_plugin_config_path.mkdir(exist_ok=True) + + dir_util._path_created = {} + + if not (op_plugin_path / "Binaries").is_dir() \ + or not (op_plugin_path / "Intermediate").is_dir(): + self.installing.emit("Building the plugin ...") + print("--- Building the plugin...") + + self._build_and_move_plugin(op_plugin_path) + + self.finished.emit("Plugin successfully installed") diff --git a/openpype/widgets/README.md b/openpype/widgets/README.md new file mode 100644 index 0000000000..cda83a95d3 --- /dev/null +++ b/openpype/widgets/README.md @@ -0,0 +1,102 @@ +# Widgets + +## Splash Screen + +This widget is used for executing a monitoring progress of a process which has been executed on a different thread. + +To properly use this widget certain preparation has to be done in order to correctly execute the process and show the +splash screen. + +### Prerequisites + +In order to run a function or an operation on another thread, a `QtCore.QObject` class needs to be created with the +desired code. The class has to have a method as an entry point for the thread to execute the code. + +For utilizing the functionalities of the splash screen, certain signals need to be declared to let it know what is +happening in the thread and how is it progressing. It is also recommended to have a function to set up certain variables +which are needed in the worker's code + +For example: +```python +from qtpy import QtCore + +class ExampleWorker(QtCore.QObject): + + finished = QtCore.Signal() + failed = QtCore.Signal(str) + progress = QtCore.Signal(int) + log = QtCore.Signal(str) + stage_begin = QtCore.Signal(str) + + foo = None + bar = None + + def run(self): + # The code goes here + print("Hello world!") + self.finished.emit() + + def setup(self, + foo: str, + bar: str,): + self.foo = foo + self.bar = bar +``` + +### Creating the splash screen + +```python +import os +from qtpy import QtCore +from pathlib import Path +from openpype.widgets.splash_screen import SplashScreen +from openpype import resources + + +def exec_plugin_install( engine_path: Path, env: dict = None): + env = env or os.environ + q_thread = QtCore.QThread() + example_worker = ExampleWorker() + + q_thread.started.connect(example_worker.run) + example_worker.setup(engine_path, env) + example_worker.moveToThread(q_thread) + + splash_screen = SplashScreen("Executing process ...", + resources.get_openpype_icon_filepath()) + + # set up the splash screen with necessary events + example_worker.installing.connect(splash_screen.update_top_label_text) + example_worker.progress.connect(splash_screen.update_progress) + example_worker.log.connect(splash_screen.append_log) + example_worker.finished.connect(splash_screen.quit_and_close) + example_worker.failed.connect(splash_screen.fail) + + splash_screen.start_thread(q_thread) + splash_screen.show_ui() +``` + +In this example code, before executing the process the worker needs to be instantiated and moved onto a newly created +`QtCore.QThread` object. After this, needed signals have to be connected to the desired slots to make full use of +the splash screen. Finally, the `start_thread` and `show_ui` is called. + +**Note that when the `show_ui` function is called the thread is blocked until the splash screen quits automatically, or +it is closed by the user in case the process fails! The `start_thread` method in that case must be called before +showing the UI!** + +The most important signals are +```python +q_thread.started.connect(example_worker.run) +``` + and +```python +example_worker.finished.connect(splash_screen.quit_and_close) +``` + +These ensure that when the `start_thread` method is called (which takes as a parameter the `QtCore.QThread` object and +saves it as a reference), the `QThread` object starts and signals the worker to +start executing its own code. Once the worker is done and emits a signal that it has finished with the `quit_and_close` +slot, the splash screen quits the `QtCore.QThread` and closes itself. + +It is highly recommended to also use the `fail` slot in case an exception or other error occurs during the execution of +the worker's code (You would use in this case the `failed` signal in the `ExampleWorker`). diff --git a/openpype/widgets/splash_screen.py b/openpype/widgets/splash_screen.py new file mode 100644 index 0000000000..4a7598180d --- /dev/null +++ b/openpype/widgets/splash_screen.py @@ -0,0 +1,253 @@ +from qtpy import QtWidgets, QtCore, QtGui +from openpype import style, resources +from igniter.nice_progress_bar import NiceProgressBar + + +class SplashScreen(QtWidgets.QDialog): + """Splash screen for executing a process on another thread. It is able + to inform about the progress of the process and log given information. + """ + + splash_icon = None + top_label = None + show_log_btn: QtWidgets.QLabel = None + progress_bar = None + log_text: QtWidgets.QLabel = None + scroll_area: QtWidgets.QScrollArea = None + close_btn: QtWidgets.QPushButton = None + scroll_bar: QtWidgets.QScrollBar = None + + is_log_visible = False + is_scroll_auto = True + + thread_return_code = None + q_thread: QtCore.QThread = None + + def __init__(self, + window_title: str, + splash_icon=None, + window_icon=None): + """ + Args: + window_title (str): String which sets the window title + splash_icon (str | bytes | None): A resource (pic) which is used for + the splash icon + window_icon (str | bytes | None: A resource (pic) which is used for + the window's icon + """ + super(SplashScreen, self).__init__() + + if splash_icon is None: + splash_icon = resources.get_openpype_icon_filepath() + + if window_icon is None: + window_icon = resources.get_openpype_icon_filepath() + + self.splash_icon = splash_icon + self.setWindowIcon(QtGui.QIcon(window_icon)) + self.setWindowTitle(window_title) + self.init_ui() + + def was_proc_successful(self) -> bool: + if self.thread_return_code == 0: + return True + return False + + def start_thread(self, q_thread: QtCore.QThread): + """Saves the reference to this thread and starts it. + + Args: + q_thread (QtCore.QThread): A QThread containing a given worker + (QtCore.QObject) + + Returns: + None + """ + if not q_thread: + raise RuntimeError("Failed to run a worker thread! The thread is null!") + + self.q_thread = q_thread + self.q_thread.start() + + @QtCore.Slot() + def quit_and_close(self): + """Quits the thread and closes the splash screen. Note that this means + the thread has exited with the return code 0! + + Returns: + None + """ + self.thread_return_code = 0 + self.q_thread.quit() + self.close() + + @QtCore.Slot() + def toggle_log(self): + if self.is_log_visible: + self.scroll_area.hide() + width = self.width() + self.adjustSize() + self.resize(width, self.height()) + else: + self.scroll_area.show() + self.scroll_bar.setValue(self.scroll_bar.maximum()) + self.resize(self.width(), 300) + + self.is_log_visible = not self.is_log_visible + + def show_ui(self): + """Shows the splash screen. BEWARE THAT THIS FUNCTION IS BLOCKING + (The execution of code can not proceed further beyond this function + until the splash screen is closed!) + + Returns: + None + """ + self.show() + self.exec_() + + def init_ui(self): + self.resize(450, 100) + self.setMinimumWidth(250) + self.setStyleSheet(style.load_stylesheet()) + + # Top Section + self.top_label = QtWidgets.QLabel(self); + self.top_label.setText("Starting process ...") + self.top_label.setWordWrap(True) + + icon = QtWidgets.QLabel(self) + icon.setPixmap(QtGui.QPixmap(self.splash_icon)) + icon.setFixedHeight(45) + icon.setFixedWidth(45) + icon.setScaledContents(True) + + self.close_btn = QtWidgets.QPushButton(self) + self.close_btn.setText("Quit") + self.close_btn.clicked.connect(self.close) + self.close_btn.setFixedWidth(80) + self.close_btn.hide() + + self.show_log_btn = QtWidgets.QPushButton(self) + self.show_log_btn.setText("Show log") + self.show_log_btn.setFixedWidth(80) + self.show_log_btn.clicked.connect(self.toggle_log) + + button_layout = QtWidgets.QVBoxLayout() + button_layout.addWidget(self.show_log_btn) + button_layout.addWidget(self.close_btn) + + # Progress Bar + self.progress_bar = NiceProgressBar() + self.progress_bar.setValue(0) + self.progress_bar.setAlignment(QtCore.Qt.AlignTop) + + # Log Content + self.scroll_area = QtWidgets.QScrollArea(self) + self.scroll_area.hide() + log_widget = QtWidgets.QWidget(self.scroll_area) + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll_area.setWidget(log_widget) + + self.scroll_bar = self.scroll_area.verticalScrollBar() + self.scroll_bar.sliderMoved.connect(self.on_scroll) + + self.log_text = QtWidgets.QLabel(self) + self.log_text.setText('') + self.log_text.setAlignment(QtCore.Qt.AlignTop) + + log_layout = QtWidgets.QVBoxLayout(log_widget) + log_layout.addWidget(self.log_text) + + top_layout = QtWidgets.QHBoxLayout() + top_layout.setAlignment(QtCore.Qt.AlignTop) + top_layout.addWidget(icon) + top_layout.addSpacing(10) + top_layout.addWidget(self.top_label) + top_layout.addSpacing(10) + top_layout.addLayout(button_layout) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.addLayout(top_layout) + main_layout.addSpacing(10) + main_layout.addWidget(self.progress_bar) + main_layout.addSpacing(10) + main_layout.addWidget(self.scroll_area) + + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint + ) + + desktop_rect = QtWidgets.QApplication.desktop().availableGeometry(self) + center = desktop_rect.center() + self.move( + center.x() - (self.width() * 0.5), + center.y() - (self.height() * 0.5) + ) + + @QtCore.Slot(int) + def update_progress(self, value: int): + self.progress_bar.setValue(value) + + @QtCore.Slot(str) + def update_top_label_text(self, text: str): + self.top_label.setText(text) + + @QtCore.Slot(str, str) + def append_log(self, text: str, end: str = ''): + """A slot used for receiving log info and appending it to scroll area's + content. + Args: + text (str): A log text that will append to the current one in the scroll + area. + end (str): end string which can be appended to the end of the given + line (for ex. a line break). + + Returns: + None + """ + self.log_text.setText(self.log_text.text() + text + end) + if self.is_scroll_auto: + self.scroll_bar.setValue(self.scroll_bar.maximum()) + + @QtCore.Slot(int) + def on_scroll(self, position: int): + """ + A slot for the vertical scroll bar's movement. This ensures the + auto-scrolling feature of the scroll area when the scroll bar is at its + maximum value. + + Args: + position (int): Position value of the scroll bar. + + Returns: + None + """ + if self.scroll_bar.maximum() == position: + self.is_scroll_auto = True + return + + self.is_scroll_auto = False + + @QtCore.Slot(str, int) + def fail(self, text: str, return_code: int = 1): + """ + A slot used for signals which can emit when a worker (process) has + failed. at this moment the splash screen doesn't close by itself. + it has to be closed by the user. + + Args: + text (str): A text which can be set to the top label. + + Returns: + return_code (int): Return code of the thread's code + """ + self.top_label.setText(text) + self.close_btn.show() + self.thread_return_code = return_code + self.q_thread.exit(return_code) From 57faf21309a5271cc6845e674311abb7a2eff06d Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 8 Mar 2023 17:18:54 +0100 Subject: [PATCH 153/160] Cleaned up the code, fixed the hanging thread --- .../unreal/hooks/pre_workfile_preparation.py | 21 +++++++++++++------ openpype/hosts/unreal/lib.py | 4 ++-- openpype/hosts/unreal/ue_workers.py | 20 +++++++----------- openpype/widgets/splash_screen.py | 15 ++++++++----- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 8ede80f7fd..c3f9ea7e72 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -5,7 +5,10 @@ import copy from pathlib import Path from openpype.widgets.splash_screen import SplashScreen from qtpy import QtCore -from openpype.hosts.unreal.ue_workers import UEProjectGenerationWorker, UEPluginInstallWorker +from openpype.hosts.unreal.ue_workers import ( + UEProjectGenerationWorker, + UEPluginInstallWorker +) from openpype import resources from openpype.lib import ( @@ -73,8 +76,10 @@ class UnrealPrelaunchHook(PreLaunchHook): ue_plugin_worker.setup(engine_path, env) ue_plugin_worker.moveToThread(q_thread) - splash_screen = SplashScreen("Installing plugin", - resources.get_resource("app_icons", "ue4.png")) + splash_screen = SplashScreen( + "Installing plugin", + resources.get_resource("app_icons", "ue4.png") + ) # set up the splash screen with necessary triggers ue_plugin_worker.installing.connect(splash_screen.update_top_label_text) @@ -111,10 +116,14 @@ class UnrealPrelaunchHook(PreLaunchHook): ue_project_worker.moveToThread(q_thread) q_thread.started.connect(ue_project_worker.run) - splash_screen = SplashScreen("Initializing UE project", - resources.get_resource("app_icons", "ue4.png")) + splash_screen = SplashScreen( + "Initializing UE project", + resources.get_resource("app_icons", "ue4.png") + ) - ue_project_worker.stage_begin.connect(splash_screen.update_top_label_text) + ue_project_worker.stage_begin.connect( + splash_screen.update_top_label_text + ) ue_project_worker.progress.connect(splash_screen.update_progress) ue_project_worker.log.connect(splash_screen.append_log) ue_project_worker.finished.connect(splash_screen.quit_and_close) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 08cc57a6cd..86ce0bb033 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -252,7 +252,7 @@ def create_unreal_project(project_name: str, with open(project_file.as_posix(), mode="r+") as pf: pf_json = json.load(pf) - pf_json["EngineAssociation"] = _get_build_id(engine_path, ue_version) + pf_json["EngineAssociation"] = get_build_id(engine_path, ue_version) pf.seek(0) json.dump(pf_json, pf, indent=4) pf.truncate() @@ -338,7 +338,7 @@ def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: return Path(u_build_tool_path) -def _get_build_id(engine_path: Path, ue_version: str) -> str: +def get_build_id(engine_path: Path, ue_version: str) -> str: ue_modules = Path() if platform.system().lower() == "windows": ue_modules_path = engine_path / "Engine/Binaries/Win64" diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 35735fcaa1..7dd08144e6 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -64,19 +64,6 @@ class UEProjectGenerationWorker(QtCore.QObject): self.engine_path = engine_path def run(self): - - - ue_id = ".".join(self.ue_version.split(".")[:2]) - # get unreal engine identifier - # ------------------------------------------------------------------------- - # FIXME (antirotor): As of 4.26 this is problem with UE4 built from - # sources. In that case Engine ID is calculated per machine/user and not - # from Engine files as this code then reads. This then prevents UE4 - # to directly open project as it will complain about project being - # created in different UE4 version. When user convert such project - # to his UE4 version, Engine ID is replaced in uproject file. If some - # other user tries to open it, it will present him with similar error. - # engine_path should be the location of UE_X.X folder ue_editor_exe = ue_lib.get_editor_exe_path(self.engine_path, @@ -122,10 +109,17 @@ class UEProjectGenerationWorker(QtCore.QObject): self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1 out' f' of {stage_count}') + if not project_file.is_file(): + msg = "Failed to write the Engine ID into .uproject file! Can " \ + "not read!" + self.failed.emit(msg) + raise RuntimeError(msg) + with open(project_file.as_posix(), mode="r+") as pf: pf_json = json.load(pf) pf_json["EngineAssociation"] = ue_lib.get_build_id(self.engine_path, self.ue_version) + print(pf_json["EngineAssociation"]) pf.seek(0) json.dump(pf_json, pf, indent=4) pf.truncate() diff --git a/openpype/widgets/splash_screen.py b/openpype/widgets/splash_screen.py index 4a7598180d..6af19e991c 100644 --- a/openpype/widgets/splash_screen.py +++ b/openpype/widgets/splash_screen.py @@ -64,7 +64,8 @@ class SplashScreen(QtWidgets.QDialog): None """ if not q_thread: - raise RuntimeError("Failed to run a worker thread! The thread is null!") + raise RuntimeError("Failed to run a worker thread! " + "The thread is null!") self.q_thread = q_thread self.q_thread.start() @@ -147,8 +148,12 @@ class SplashScreen(QtWidgets.QDialog): self.scroll_area.hide() log_widget = QtWidgets.QWidget(self.scroll_area) self.scroll_area.setWidgetResizable(True) - self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) + self.scroll_area.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarAlwaysOn + ) + self.scroll_area.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarAlwaysOn + ) self.scroll_area.setWidget(log_widget) self.scroll_bar = self.scroll_area.verticalScrollBar() @@ -203,8 +208,8 @@ class SplashScreen(QtWidgets.QDialog): """A slot used for receiving log info and appending it to scroll area's content. Args: - text (str): A log text that will append to the current one in the scroll - area. + text (str): A log text that will append to the current one in the + scroll area. end (str): end string which can be appended to the end of the given line (for ex. a line break). From 78d737e4b30e1a890ff0ca6d085050d88bdedc6b Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 8 Mar 2023 17:20:50 +0100 Subject: [PATCH 154/160] Reformatted the file --- openpype/widgets/splash_screen.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/widgets/splash_screen.py b/openpype/widgets/splash_screen.py index 6af19e991c..fffe143ea5 100644 --- a/openpype/widgets/splash_screen.py +++ b/openpype/widgets/splash_screen.py @@ -30,8 +30,8 @@ class SplashScreen(QtWidgets.QDialog): """ Args: window_title (str): String which sets the window title - splash_icon (str | bytes | None): A resource (pic) which is used for - the splash icon + splash_icon (str | bytes | None): A resource (pic) which is used + for the splash icon window_icon (str | bytes | None: A resource (pic) which is used for the window's icon """ @@ -113,7 +113,7 @@ class SplashScreen(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) # Top Section - self.top_label = QtWidgets.QLabel(self); + self.top_label = QtWidgets.QLabel(self) self.top_label.setText("Starting process ...") self.top_label.setWordWrap(True) From fc67c5a2c0b4d5a3bd4abae3ab860cc9f81a3762 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 8 Mar 2023 17:23:37 +0100 Subject: [PATCH 155/160] Fixed the line indentation. --- openpype/hosts/unreal/ue_workers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 7dd08144e6..2162357912 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -310,8 +310,8 @@ class UEPluginInstallWorker(QtCore.QObject): raise RuntimeError(msg) # Create a path to the plugin in the engine - op_plugin_path = self.engine_path / \ - "Engine/Plugins/Marketplace/OpenPype" + op_plugin_path = self.engine_path / "Engine/Plugins/Marketplace" \ + "/OpenPype" if not op_plugin_path.is_dir(): self.installing.emit("Installing and building the plugin ...") From e5b7349dff710bc2577e1a9adba39cde9748e231 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 8 Mar 2023 17:26:27 +0100 Subject: [PATCH 156/160] Code cleanup --- openpype/hosts/unreal/ue_workers.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index 2162357912..00f83a7d7a 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -106,8 +106,8 @@ class UEProjectGenerationWorker(QtCore.QObject): raise RuntimeError(msg) print("--- Project has been generated successfully.") - self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1 out' - f' of {stage_count}') + self.stage_begin.emit(f'Writing the Engine ID of the build UE ... 1' + f' out of {stage_count}') if not project_file.is_file(): msg = "Failed to write the Engine ID into .uproject file! Can " \ @@ -117,8 +117,10 @@ class UEProjectGenerationWorker(QtCore.QObject): with open(project_file.as_posix(), mode="r+") as pf: pf_json = json.load(pf) - pf_json["EngineAssociation"] = ue_lib.get_build_id(self.engine_path, - self.ue_version) + pf_json["EngineAssociation"] = ue_lib.get_build_id( + self.engine_path, + self.ue_version + ) print(pf_json["EngineAssociation"]) pf.seek(0) json.dump(pf_json, pf, indent=4) @@ -132,7 +134,8 @@ class UEProjectGenerationWorker(QtCore.QObject): f'{stage_count}') self.progress.emit(0) - ubt_path = ue_lib.get_path_to_ubt(self.engine_path, self.ue_version) + ubt_path = ue_lib.get_path_to_ubt(self.engine_path, + self.ue_version) arch = "Win64" if platform.system().lower() == "windows": @@ -199,8 +202,8 @@ class UEProjectGenerationWorker(QtCore.QObject): # ensure we have PySide2 installed in engine self.progress.emit(0) - self.stage_begin.emit(f'Checking PySide2 installation... {stage_count} ' - f'out of {stage_count}') + self.stage_begin.emit(f'Checking PySide2 installation... {stage_count}' + f' out of {stage_count}') python_path = None if platform.system().lower() == "windows": python_path = self.engine_path / ("Engine/Binaries/ThirdParty/" From 7941c73f82ce7d708e7387f0db4f39df54c58f67 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 8 Mar 2023 17:27:25 +0100 Subject: [PATCH 157/160] Code cleanup --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index c3f9ea7e72..da12bc75de 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -82,7 +82,9 @@ class UnrealPrelaunchHook(PreLaunchHook): ) # set up the splash screen with necessary triggers - ue_plugin_worker.installing.connect(splash_screen.update_top_label_text) + ue_plugin_worker.installing.connect( + splash_screen.update_top_label_text + ) ue_plugin_worker.progress.connect(splash_screen.update_progress) ue_plugin_worker.log.connect(splash_screen.append_log) ue_plugin_worker.finished.connect(splash_screen.quit_and_close) From 3b64f515b25ebeccd4235a02722aa1e1eef26bc0 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 11 Mar 2023 03:26:36 +0000 Subject: [PATCH 158/160] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c7a5e9bea5..e8124f1466 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.2-nightly.5" +__version__ = "3.15.2-nightly.6" From a7f0d576384a5dabcaf725c10632472e08b9c0a0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 13 Mar 2023 10:32:19 +0100 Subject: [PATCH 159/160] SiteSync: host dirmap is not working properly (#4563) - only editable keys are returned from Site Sync module to Local Settings Cleaner approach even if LS UI is going away in Ayon. - use remote_site only if is local_drive provider - remove unwanted import - cache mapping, update logging Mapping was called multiple times for Nuke. Logging was too verbose. --- openpype/host/dirmap.py | 63 +++++++++---------- openpype/hosts/nuke/api/lib.py | 54 +++++++++------- .../modules/sync_server/sync_server_module.py | 20 +++++- .../local_settings/projects_widget.py | 2 +- 4 files changed, 81 insertions(+), 58 deletions(-) diff --git a/openpype/host/dirmap.py b/openpype/host/dirmap.py index 347c5fbf85..1d084cccad 100644 --- a/openpype/host/dirmap.py +++ b/openpype/host/dirmap.py @@ -39,7 +39,6 @@ class HostDirmap(object): self._project_settings = project_settings self._sync_module = sync_module # to limit reinit of Modules self._log = None - self._mapping = None # cache mapping @property def sync_module(self): @@ -70,29 +69,28 @@ class HostDirmap(object): """Run host dependent remapping from source_path to destination_path""" pass - def process_dirmap(self): + def process_dirmap(self, mapping=None): # type: (dict) -> None """Go through all paths in Settings and set them using `dirmap`. If artists has Site Sync enabled, take dirmap mapping directly from Local Settings when artist is syncing workfile locally. - Args: - project_settings (dict): Settings for current project. """ - if not self._mapping: - self._mapping = self.get_mappings(self.project_settings) - if not self._mapping: + if not mapping: + mapping = self.get_mappings() + if not mapping: return - self.log.info("Processing directory mapping ...") self.on_enable_dirmap() - self.log.info("mapping:: {}".format(self._mapping)) - for k, sp in enumerate(self._mapping["source-path"]): - dst = self._mapping["destination-path"][k] + for k, sp in enumerate(mapping["source-path"]): + dst = mapping["destination-path"][k] try: + # add trailing slash if missing + sp = os.path.join(sp, '') + dst = os.path.join(dst, '') print("{} -> {}".format(sp, dst)) self.dirmap_routine(sp, dst) except IndexError: @@ -110,28 +108,24 @@ class HostDirmap(object): ) continue - def get_mappings(self, project_settings): + def get_mappings(self): """Get translation from source-path to destination-path. It checks if Site Sync is enabled and user chose to use local site, in that case configuration in Local Settings takes precedence """ - local_mapping = self._get_local_sync_dirmap(project_settings) dirmap_label = "{}-dirmap".format(self.host_name) - if ( - not self.project_settings[self.host_name].get(dirmap_label) - and not local_mapping - ): - return {} - mapping_settings = self.project_settings[self.host_name][dirmap_label] - mapping_enabled = mapping_settings["enabled"] or bool(local_mapping) + mapping_sett = self.project_settings[self.host_name].get(dirmap_label, + {}) + local_mapping = self._get_local_sync_dirmap() + mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping) if not mapping_enabled: return {} mapping = ( local_mapping - or mapping_settings["paths"] + or mapping_sett["paths"] or {} ) @@ -141,28 +135,27 @@ class HostDirmap(object): or not mapping.get("source-path") ): return {} + self.log.info("Processing directory mapping ...") + self.log.info("mapping:: {}".format(mapping)) return mapping - def _get_local_sync_dirmap(self, project_settings): + def _get_local_sync_dirmap(self): """ Returns dirmap if synch to local project is enabled. Only valid mapping is from roots of remote site to local site set in Local Settings. - Args: - project_settings (dict) Returns: dict : { "source-path": [XXX], "destination-path": [YYYY]} """ + project_name = os.getenv("AVALON_PROJECT") mapping = {} - - if not project_settings["global"]["sync_server"]["enabled"]: + if (not self.sync_module.enabled or + project_name not in self.sync_module.get_enabled_projects()): return mapping - project_name = os.getenv("AVALON_PROJECT") - active_site = self.sync_module.get_local_normalized_site( self.sync_module.get_active_site(project_name)) remote_site = self.sync_module.get_local_normalized_site( @@ -171,11 +164,7 @@ class HostDirmap(object): "active {} - remote {}".format(active_site, remote_site) ) - if ( - active_site == "local" - and project_name in self.sync_module.get_enabled_projects() - and active_site != remote_site - ): + if active_site == "local" and active_site != remote_site: sync_settings = self.sync_module.get_sync_project_setting( project_name, exclude_locals=False, @@ -188,7 +177,15 @@ class HostDirmap(object): self.log.debug("local overrides {}".format(active_overrides)) self.log.debug("remote overrides {}".format(remote_overrides)) + current_platform = platform.system().lower() + remote_provider = self.sync_module.get_provider_for_site( + project_name, remote_site + ) + # dirmap has sense only with regular disk provider, in the workfile + # wont be root on cloud or sftp provider + if remote_provider != "local_drive": + remote_site = "studio" for root_name, active_site_dir in active_overrides.items(): remote_site_dir = ( remote_overrides.get(root_name) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a5a631cc70..2a14096f0e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2861,10 +2861,10 @@ class NukeDirmap(HostDirmap): pass def dirmap_routine(self, source_path, destination_path): - log.debug("{}: {}->{}".format(self.file_name, - source_path, destination_path)) source_path = source_path.lower().replace(os.sep, '/') destination_path = destination_path.lower().replace(os.sep, '/') + log.debug("Map: {} with: {}->{}".format(self.file_name, + source_path, destination_path)) if platform.system().lower() == "windows": self.file_name = self.file_name.lower().replace( source_path, destination_path) @@ -2878,6 +2878,7 @@ class DirmapCache: _project_name = None _project_settings = None _sync_module = None + _mapping = None @classmethod def project_name(cls): @@ -2897,6 +2898,36 @@ class DirmapCache: cls._sync_module = ModulesManager().modules_by_name["sync_server"] return cls._sync_module + @classmethod + def mapping(cls): + return cls._mapping + + @classmethod + def set_mapping(cls, mapping): + cls._mapping = mapping + + +def dirmap_file_name_filter(file_name): + """Nuke callback function with single full path argument. + + Checks project settings for potential mapping from source to dest. + """ + + dirmap_processor = NukeDirmap( + file_name, + "nuke", + DirmapCache.project_name(), + DirmapCache.project_settings(), + DirmapCache.sync_module(), + ) + if not DirmapCache.mapping(): + DirmapCache.set_mapping(dirmap_processor.get_mappings()) + + dirmap_processor.process_dirmap(DirmapCache.mapping()) + if os.path.exists(dirmap_processor.file_name): + return dirmap_processor.file_name + return file_name + @contextlib.contextmanager def node_tempfile(): @@ -2942,25 +2973,6 @@ def duplicate_node(node): return dupli_node -def dirmap_file_name_filter(file_name): - """Nuke callback function with single full path argument. - - Checks project settings for potential mapping from source to dest. - """ - - dirmap_processor = NukeDirmap( - file_name, - "nuke", - DirmapCache.project_name(), - DirmapCache.project_settings(), - DirmapCache.sync_module(), - ) - dirmap_processor.process_dirmap() - if os.path.exists(dirmap_processor.file_name): - return dirmap_processor.file_name - return file_name - - def get_group_io_nodes(nodes): """Get the input and the output of a group of nodes.""" diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 28863c091a..5a4fa07e98 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -1472,13 +1472,15 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return sync_settings - def get_all_site_configs(self, project_name=None): + def get_all_site_configs(self, project_name=None, + local_editable_only=False): """ Returns (dict) with all sites configured system wide. Args: project_name (str)(optional): if present, check if not disabled - + local_editable_only (bool)(opt): if True return only Local + Setting configurable (for LS UI) Returns: (dict): {'studio': {'provider':'local_drive'...}, 'MY_LOCAL': {'provider':....}} @@ -1499,9 +1501,21 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if site_settings: detail.update(site_settings) system_sites[site] = detail - system_sites.update(self._get_default_site_configs(sync_enabled, project_name)) + if local_editable_only: + local_schema = SyncServerModule.get_local_settings_schema() + editable_keys = {} + for provider_code, editables in local_schema.items(): + editable_keys[provider_code] = ["enabled", "provider"] + for editable_item in editables: + editable_keys[provider_code].append(editable_item["key"]) + + for _, site in system_sites.items(): + provider = site["provider"] + for site_config_key in list(site.keys()): + if site_config_key not in editable_keys[provider]: + site.pop(site_config_key, None) return system_sites diff --git a/openpype/tools/settings/local_settings/projects_widget.py b/openpype/tools/settings/local_settings/projects_widget.py index bdf291524c..4a4148d7cd 100644 --- a/openpype/tools/settings/local_settings/projects_widget.py +++ b/openpype/tools/settings/local_settings/projects_widget.py @@ -272,7 +272,7 @@ class SitesWidget(QtWidgets.QWidget): ) site_configs = sync_server_module.get_all_site_configs( - self._project_name) + self._project_name, local_editable_only=True) roots_entity = ( self.project_settings[PROJECT_ANATOMY_KEY][LOCAL_ROOTS_KEY] From 86184a8ee0525c3f361850cae0b576aca051bdf4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 13 Mar 2023 11:20:59 +0100 Subject: [PATCH 160/160] Tools: Fix recursive filtering (#4597) * check for 'filterRegExp' first * use recursive filtering option if is available --- openpype/tools/loader/widgets.py | 6 +++--- openpype/tools/sceneinventory/model.py | 6 +++--- openpype/tools/sceneinventory/window.py | 6 +++--- .../tools/settings/settings/search_dialog.py | 12 +++++------ .../model_filter_proxy_recursive_sort.py | 6 +++--- openpype/tools/utils/models.py | 20 ++++++++++++++----- 6 files changed, 33 insertions(+), 23 deletions(-) diff --git a/openpype/tools/loader/widgets.py b/openpype/tools/loader/widgets.py index 98ac9c871f..b3aa381d14 100644 --- a/openpype/tools/loader/widgets.py +++ b/openpype/tools/loader/widgets.py @@ -295,10 +295,10 @@ class SubsetWidget(QtWidgets.QWidget): self.model.set_grouping(state) def _subset_changed(self, text): - if hasattr(self.proxy, "setFilterRegularExpression"): - self.proxy.setFilterRegularExpression(text) - else: + if hasattr(self.proxy, "setFilterRegExp"): self.proxy.setFilterRegExp(text) + else: + self.proxy.setFilterRegularExpression(text) self.view.expandAll() def set_loading_state(self, loading, empty): diff --git a/openpype/tools/sceneinventory/model.py b/openpype/tools/sceneinventory/model.py index 3398743aec..680dfd5a51 100644 --- a/openpype/tools/sceneinventory/model.py +++ b/openpype/tools/sceneinventory/model.py @@ -482,10 +482,10 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): return True # Filter by regex - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern: pattern = re.escape(pattern) diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 8a6e43f796..89424fd746 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -160,10 +160,10 @@ class SceneInventoryWindow(QtWidgets.QDialog): self._model.set_hierarchy_view(enabled) def _on_text_filter_change(self, text_filter): - if hasattr(self._proxy, "setFilterRegularExpression"): - self._proxy.setFilterRegularExpression(text_filter) - else: + if hasattr(self._proxy, "setFilterRegExp"): self._proxy.setFilterRegExp(text_filter) + else: + self._proxy.setFilterRegularExpression(text_filter) def _on_outdated_state_change(self): self._proxy.set_filter_outdated( diff --git a/openpype/tools/settings/settings/search_dialog.py b/openpype/tools/settings/settings/search_dialog.py index 33a4d16e98..59750c02e1 100644 --- a/openpype/tools/settings/settings/search_dialog.py +++ b/openpype/tools/settings/settings/search_dialog.py @@ -27,10 +27,10 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): if not parent.isValid(): return False - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern and regex.isValid(): @@ -111,10 +111,10 @@ class SearchEntitiesDialog(QtWidgets.QDialog): def _on_filter_timer(self): text = self._filter_edit.text() - if hasattr(self._proxy, "setFilterRegularExpression"): - self._proxy.setFilterRegularExpression(text) - else: + if hasattr(self._proxy, "setFilterRegExp"): self._proxy.setFilterRegExp(text) + else: + self._proxy.setFilterRegularExpression(text) # WARNING This expanding and resizing is relatively slow. self._view.expandAll() diff --git a/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py b/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py index 5c72e2049b..602faaa489 100644 --- a/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py +++ b/openpype/tools/standalonepublish/widgets/model_filter_proxy_recursive_sort.py @@ -5,10 +5,10 @@ from qtpy import QtCore class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): """Filters to the regex if any of the children matches allow parent""" def filterAcceptsRow(self, row, parent): - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern: model = self.sourceModel() diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index 270e00b2ef..94645af110 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -202,11 +202,20 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): Use case: Filtering by string - parent won't be filtered if does not match the filter string but first checks if any children does. """ + + def __init__(self, *args, **kwargs): + super(RecursiveSortFilterProxyModel, self).__init__(*args, **kwargs) + recursive_enabled = False + if hasattr(self, "setRecursiveFilteringEnabled"): + self.setRecursiveFilteringEnabled(True) + recursive_enabled = True + self._recursive_enabled = recursive_enabled + def filterAcceptsRow(self, row, parent_index): - if hasattr(self, "filterRegularExpression"): - regex = self.filterRegularExpression() - else: + if hasattr(self, "filterRegExp"): regex = self.filterRegExp() + else: + regex = self.filterRegularExpression() pattern = regex.pattern() if pattern: @@ -219,8 +228,9 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): # Check current index itself value = model.data(source_index, self.filterRole()) - if re.search(pattern, value, re.IGNORECASE): - return True + matched = bool(re.search(pattern, value, re.IGNORECASE)) + if matched or self._recursive_enabled: + return matched rows = model.rowCount(source_index) for idx in range(rows):