From e2076c0f2fa264f4dc7ae6959c3292b60616ca91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 3 Feb 2022 19:03:16 +0100 Subject: [PATCH 001/194] Module: Kitsu module --- .../modules/default_modules/kitsu/__init__.py | 15 ++ .../default_modules/kitsu/kitsu_module.py | 147 ++++++++++++++++++ .../kitsu/plugins/publish/example_plugin.py | 9 ++ .../schemas/project_schemas/main.json | 30 ++++ .../schemas/project_schemas/the_template.json | 30 ++++ .../modules/default_modules/kitsu/widgets.py | 31 ++++ .../defaults/project_settings/kitsu.json | 3 + .../defaults/system_settings/modules.json | 4 + .../schemas/projects_schema/schema_main.json | 4 + .../projects_schema/schema_project_kitsu.json | 17 ++ .../module_settings/schema_kitsu.json | 23 +++ .../schemas/system_schema/schema_modules.json | 4 + 12 files changed, 317 insertions(+) create mode 100644 openpype/modules/default_modules/kitsu/__init__.py create mode 100644 openpype/modules/default_modules/kitsu/kitsu_module.py create mode 100644 openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py create mode 100644 openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json create mode 100644 openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json create mode 100644 openpype/modules/default_modules/kitsu/widgets.py create mode 100644 openpype/settings/defaults/project_settings/kitsu.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json create mode 100644 openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json diff --git a/openpype/modules/default_modules/kitsu/__init__.py b/openpype/modules/default_modules/kitsu/__init__.py new file mode 100644 index 0000000000..cd0c2ea8af --- /dev/null +++ b/openpype/modules/default_modules/kitsu/__init__.py @@ -0,0 +1,15 @@ +""" Addon class definition and Settings definition must be imported here. + +If addon class or settings definition won't be here their definition won't +be found by OpenPype discovery. +""" + +from .kitsu_module import ( + AddonSettingsDef, + KitsuModule +) + +__all__ = ( + "AddonSettingsDef", + "KitsuModule" +) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py new file mode 100644 index 0000000000..81d7e56a81 --- /dev/null +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -0,0 +1,147 @@ +"""Addon definition is located here. + +Import of python packages that may not be available should not be imported +in global space here until are required or used. +- Qt related imports +- imports of Python 3 packages + - we still support Python 2 hosts where addon definition should available +""" + +import os +import click + +from openpype.modules import ( + JsonFilesSettingsDef, + OpenPypeModule, + ModulesManager +) +# Import interface defined by this addon to be able find other addons using it +from openpype_interfaces import ( + IPluginPaths, + ITrayAction +) + + +# Settings definition of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definition using json files +# to define settings and store default values +class AddonSettingsDef(JsonFilesSettingsDef): + # This will add prefixes to every schema and template from `schemas` + # subfolder. + # - it is not required to fill the prefix but it is highly + # recommended as schemas and templates may have name clashes across + # multiple addons + # - it is also recommended that prefix has addon name in it + schema_prefix = "kitsu" + + def get_settings_root_path(self): + """Implemented abstract class of JsonFilesSettingsDef. + + Return directory path where json files defying addon settings are + located. + """ + return os.path.join( + os.path.dirname(os.path.abspath(__file__)), + "settings" + ) + + +class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): + """This Addon has defined it's settings and interface. + + This example has system settings with an enabled option. And use + few other interfaces: + - `IPluginPaths` to define custom plugin paths + - `ITrayAction` to be shown in tray tool + """ + label = "Kitsu" + name = "kitsu" + + def initialize(self, settings): + """Initialization of addon.""" + module_settings = settings[self.name] + # Enabled by settings + self.enabled = module_settings.get("enabled", False) + + # Prepare variables that can be used or set afterwards + self._connected_modules = None + # UI which must not be created at this time + self._dialog = None + + def tray_init(self): + """Implementation of abstract method for `ITrayAction`. + + We're definitely in tray tool so we can pre create dialog. + """ + + self._create_dialog() + + def _create_dialog(self): + # Don't recreate dialog if already exists + if self._dialog is not None: + return + + from .widgets import MyExampleDialog + + self._dialog = MyExampleDialog() + + def show_dialog(self): + """Show dialog with connected modules. + + This can be called from anywhere but can also crash in headless mode. + There is no way to prevent addon to do invalid operations if he's + not handling them. + """ + # Make sure dialog is created + self._create_dialog() + # Show dialog + self._dialog.open() + + def get_connected_modules(self): + """Custom implementation of addon.""" + names = set() + if self._connected_modules is not None: + for module in self._connected_modules: + names.add(module.name) + return names + + def on_action_trigger(self): + """Implementation of abstract method for `ITrayAction`.""" + self.show_dialog() + + def get_plugin_paths(self): + """Implementation of abstract method for `IPluginPaths`.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + + return { + "publish": [os.path.join(current_dir, "plugins", "publish")] + } + + def cli(self, click_group): + click_group.add_command(cli_main) + + +@click.group(KitsuModule.name, help="Kitsu dynamic cli commands.") +def cli_main(): + pass + + +@cli_main.command() +def nothing(): + """Does nothing but print a message.""" + print("You've triggered \"nothing\" command.") + + +@cli_main.command() +def show_dialog(): + """Show ExampleAddon dialog. + + We don't have access to addon directly through cli so we have to create + it again. + """ + from openpype.tools.utils.lib import qt_app_context + + manager = ModulesManager() + example_addon = manager.modules_by_name[KitsuModule.name] + with qt_app_context(): + example_addon.show_dialog() diff --git a/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py b/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py new file mode 100644 index 0000000000..61602f4e78 --- /dev/null +++ b/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py @@ -0,0 +1,9 @@ +import pyblish.api + + +class CollectExampleAddon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Kitsu" + + def process(self, context): + self.log.info("I'm in Kitsu's plugin!") diff --git a/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json b/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json new file mode 100644 index 0000000000..82e58ce9ab --- /dev/null +++ b/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json @@ -0,0 +1,30 @@ +{ + "type": "dict", + "key": "kitsu", + "label": " Kitsu", + "collapsible": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + }, + { + "type": "template", + "name": "kitsu/the_template", + "template_data": [ + { + "name": "color_1", + "label": "Color 1" + }, + { + "name": "color_2", + "label": "Color 2" + } + ] + } + ] +} diff --git a/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json b/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json new file mode 100644 index 0000000000..af8fd9dae4 --- /dev/null +++ b/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json @@ -0,0 +1,30 @@ +[ + { + "type": "list-strict", + "key": "{name}", + "label": "{label}", + "object_types": [ + { + "label": "Red", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Green", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + }, + { + "label": "Blue", + "type": "number", + "minimum": 0, + "maximum": 1, + "decimal": 3 + } + ] + } +] diff --git a/openpype/modules/default_modules/kitsu/widgets.py b/openpype/modules/default_modules/kitsu/widgets.py new file mode 100644 index 0000000000..de232113fe --- /dev/null +++ b/openpype/modules/default_modules/kitsu/widgets.py @@ -0,0 +1,31 @@ +from Qt import QtWidgets + +from openpype.style import load_stylesheet + + +class MyExampleDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(MyExampleDialog, self).__init__(parent) + + self.setWindowTitle("Connected modules") + + msg = "This is example dialog of Kitsu." + label_widget = QtWidgets.QLabel(msg, self) + + ok_btn = QtWidgets.QPushButton("OK", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(label_widget) + layout.addLayout(btns_layout) + + ok_btn.clicked.connect(self._on_ok_clicked) + + self._label_widget = label_widget + + self.setStyleSheet(load_stylesheet()) + + def _on_ok_clicked(self): + self.done(1) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json new file mode 100644 index 0000000000..b4d2ccc611 --- /dev/null +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -0,0 +1,3 @@ +{ + "number": 0 +} \ No newline at end of file diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index d74269922f..9cfaddecbe 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -137,6 +137,10 @@ } } }, + "kitsu": { + "enabled": false, + "kitsu_server": "" + }, "timers_manager": { "enabled": true, "auto_stop": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index dbddd18c80..6c07209de3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -62,6 +62,10 @@ "type": "schema", "name": "schema_project_ftrack" }, + { + "type": "schema", + "name": "schema_project_kitsu" + }, { "type": "schema", "name": "schema_project_deadline" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json new file mode 100644 index 0000000000..93976cc03b --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -0,0 +1,17 @@ +{ + "type": "dict", + "key": "kitsu", + "label": "Kitsu", + "collapsible": true, + "is_file": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + } + ] +} diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json new file mode 100644 index 0000000000..8e496dc783 --- /dev/null +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json @@ -0,0 +1,23 @@ +{ + "type": "dict", + "key": "kitsu", + "label": "Kitsu", + "collapsible": true, + "require_restart": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "kitsu_server", + "label": "Server" + }, + { + "type": "splitter" + } + ] +} diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 52595914ed..d22b9016a7 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -44,6 +44,10 @@ "type": "schema", "name": "schema_ftrack" }, + { + "type": "schema", + "name": "schema_kitsu" + }, { "type": "dict", "key": "timers_manager", From 70440290cafcf02753f8c499951cc164b011c132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Feb 2022 15:38:02 +0100 Subject: [PATCH 002/194] Fist step to sync from Zou to local --- .../default_modules/kitsu/kitsu_module.py | 125 +++++++++++++++++- .../defaults/system_settings/modules.json | 4 +- .../module_settings/schema_kitsu.json | 12 +- 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 81d7e56a81..55e4640fa2 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -6,20 +6,27 @@ in global space here until are required or used. - imports of Python 3 packages - we still support Python 2 hosts where addon definition should available """ - +import collections import os import click +from avalon.api import AvalonMongoDB +import gazu +from openpype.api import get_project_basic_paths, create_project_folders +from openpype.lib import create_project +from openpype.lib.anatomy import Anatomy from openpype.modules import ( JsonFilesSettingsDef, OpenPypeModule, ModulesManager ) +from openpype.tools.project_manager.project_manager.model import AssetItem, ProjectItem, TaskItem # Import interface defined by this addon to be able find other addons using it from openpype_interfaces import ( IPluginPaths, ITrayAction ) +from pymongo import UpdateOne, DeleteOne # Settings definition of this addon using `JsonFilesSettingsDef` @@ -60,9 +67,27 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): def initialize(self, settings): """Initialization of addon.""" module_settings = settings[self.name] + # Enabled by settings self.enabled = module_settings.get("enabled", False) + # Add API URL schema + kitsu_url = module_settings["server"].strip() + if kitsu_url: + # Ensure web url + if not kitsu_url.startswith("http"): + kitsu_url = "https://" + kitsu_url + + # Check for "/api" url validity + if not kitsu_url.endswith("api"): + kitsu_url = f"{kitsu_url}{'' if kitsu_url.endswith('/') else '/'}api" + + self.server_url = kitsu_url + + # Set credentials + self.script_login = module_settings["script_login"] + self.script_pwd = module_settings["script_pwd"] + # Prepare variables that can be used or set afterwards self._connected_modules = None # UI which must not be created at this time @@ -76,6 +101,14 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): self._create_dialog() + def get_global_environments(self): + """Kitsu's global environments.""" + return { + "KITSU_SERVER": self.server_url, + "KITSU_LOGIN": self.script_login, + "KITSU_PWD": self.script_pwd + } + def _create_dialog(self): # Don't recreate dialog if already exists if self._dialog is not None: @@ -127,10 +160,94 @@ def cli_main(): @cli_main.command() -def nothing(): - """Does nothing but print a message.""" - print("You've triggered \"nothing\" command.") +def sync_local(): + """Synchronize local database from Zou sever database.""" + # Connect to server + gazu.client.set_host(os.environ["KITSU_SERVER"]) + + # Authenticate + gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) + + # Iterate projects + dbcon = AvalonMongoDB() + dbcon.install() + all_projects = gazu.project.all_projects() + for project in all_projects: + # Create project locally + # Try to find project document + project_name = project["name"] + project_code = project_name + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = dbcon.find_one({ + "type": "project" + }) + + # Get all assets from zou + all_assets = gazu.asset.all_assets_for_project(project) + + # Query all assets of the local project + project_col = dbcon.database[project_code] + asset_docs_zou_ids = { + asset_doc["data"]["zou_id"] + for asset_doc in project_col.find({"type": "asset"}) + } + + # Create project if is not available + # - creation is required to be able set project anatomy and attributes + if not project_doc: + print(f"Creating project '{project_name}'") + project_doc = create_project(project_name, project_code, dbcon=dbcon) + + # Create project item + insert_list = [] + + bulk_writes = [] + for zou_asset in all_assets: + doc_data = {"zou_id": zou_asset["id"]} + + # Create Asset + new_doc = { + "name": zou_asset["name"], + "type": "asset", + "schema": "openpype:asset-3.0", + "data": doc_data, + "parent": project_doc["_id"] + } + + if zou_asset["id"] not in asset_docs_zou_ids: # Item is new + insert_list.append(new_doc) + + # TODO tasks + + # elif item.data(REMOVED_ROLE): # TODO removal + # if item.data(HIERARCHY_CHANGE_ABLE_ROLE): + # bulk_writes.append(DeleteOne( + # {"_id": item.asset_id} + # )) + # else: + # bulk_writes.append(UpdateOne( + # {"_id": item.asset_id}, + # {"$set": {"type": "archived_asset"}} + # )) + + # else: TODO update data + # update_data = new_item.update_data() + # if update_data: + # bulk_writes.append(UpdateOne( + # {"_id": new_item.asset_id}, + # update_data + # )) + + # Insert new docs if created + if insert_list: + project_col.insert_many(insert_list) + + # Write into DB TODO + # if bulk_writes: + # project_col.bulk_write(bulk_writes) + + dbcon.uninstall() @cli_main.command() def show_dialog(): diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 9cfaddecbe..ddb2edc360 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -139,7 +139,9 @@ }, "kitsu": { "enabled": false, - "kitsu_server": "" + "server": "", + "script_login": "", + "script_pwd": "" }, "timers_manager": { "enabled": true, diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json index 8e496dc783..ae2b52df0d 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json @@ -13,9 +13,19 @@ }, { "type": "text", - "key": "kitsu_server", + "key": "server", "label": "Server" }, + { + "type": "text", + "key": "script_login", + "label": "Script Login" + }, + { + "type": "text", + "key": "script_pwd", + "label": "Script Password" + }, { "type": "splitter" } From 719afdcc8ae5f8710537d55da24040bd7ccfcd95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Feb 2022 15:41:02 +0100 Subject: [PATCH 003/194] black --- .../default_modules/kitsu/kitsu_module.py | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 55e4640fa2..6eb37dfaed 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -6,27 +6,17 @@ in global space here until are required or used. - imports of Python 3 packages - we still support Python 2 hosts where addon definition should available """ -import collections import os import click from avalon.api import AvalonMongoDB import gazu -from openpype.api import get_project_basic_paths, create_project_folders from openpype.lib import create_project from openpype.lib.anatomy import Anatomy -from openpype.modules import ( - JsonFilesSettingsDef, - OpenPypeModule, - ModulesManager -) -from openpype.tools.project_manager.project_manager.model import AssetItem, ProjectItem, TaskItem +from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager + # Import interface defined by this addon to be able find other addons using it -from openpype_interfaces import ( - IPluginPaths, - ITrayAction -) -from pymongo import UpdateOne, DeleteOne +from openpype_interfaces import IPluginPaths, ITrayAction # Settings definition of this addon using `JsonFilesSettingsDef` @@ -47,10 +37,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): Return directory path where json files defying addon settings are located. """ - return os.path.join( - os.path.dirname(os.path.abspath(__file__)), - "settings" - ) + return os.path.join(os.path.dirname(os.path.abspath(__file__)), "settings") class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): @@ -61,6 +48,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): - `IPluginPaths` to define custom plugin paths - `ITrayAction` to be shown in tray tool """ + label = "Kitsu" name = "kitsu" @@ -106,7 +94,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): return { "KITSU_SERVER": self.server_url, "KITSU_LOGIN": self.script_login, - "KITSU_PWD": self.script_pwd + "KITSU_PWD": self.script_pwd, } def _create_dialog(self): @@ -146,9 +134,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Implementation of abstract method for `IPluginPaths`.""" current_dir = os.path.dirname(os.path.abspath(__file__)) - return { - "publish": [os.path.join(current_dir, "plugins", "publish")] - } + return {"publish": [os.path.join(current_dir, "plugins", "publish")]} def cli(self, click_group): click_group.add_command(cli_main) @@ -179,10 +165,8 @@ def sync_local(): project_name = project["name"] project_code = project_name dbcon.Session["AVALON_PROJECT"] = project_name - project_doc = dbcon.find_one({ - "type": "project" - }) - + project_doc = dbcon.find_one({"type": "project"}) + # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) @@ -212,7 +196,7 @@ def sync_local(): "type": "asset", "schema": "openpype:asset-3.0", "data": doc_data, - "parent": project_doc["_id"] + "parent": project_doc["_id"], } if zou_asset["id"] not in asset_docs_zou_ids: # Item is new @@ -249,6 +233,7 @@ def sync_local(): dbcon.uninstall() + @cli_main.command() def show_dialog(): """Show ExampleAddon dialog. From 7c63d3a374637ee8630d293b8ac8dcc8a34ffb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Feb 2022 16:28:06 +0100 Subject: [PATCH 004/194] Add tasks to asset --- .../default_modules/kitsu/kitsu_module.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 6eb37dfaed..9730437ec2 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -187,22 +187,43 @@ def sync_local(): insert_list = [] bulk_writes = [] - for zou_asset in all_assets: - doc_data = {"zou_id": zou_asset["id"]} + for asset in all_assets: + asset_data = {"zou_id": asset["id"]} + + # Set tasks + asset_tasks = gazu.task.all_tasks_for_asset(asset) + asset_data["tasks"] = { + t["task_type_name"]: {"type": t["task_type_name"]} for t in asset_tasks + } # Create Asset - new_doc = { - "name": zou_asset["name"], + asset_doc = { + "name": asset["name"], "type": "asset", "schema": "openpype:asset-3.0", - "data": doc_data, + "data": asset_data, "parent": project_doc["_id"], } - if zou_asset["id"] not in asset_docs_zou_ids: # Item is new - insert_list.append(new_doc) + if asset["id"] not in asset_docs_zou_ids: # Item is new + insert_list.append(asset_doc) + else: + asset_doc = project_col.find_one({"data": {"zou_id": asset["id"]}}) - # TODO tasks + # TODO update + # for task in asset_tasks: + # # print(task) + # task_data = {"zou_id": task["id"]} + + # # Create Task + # task_doc = { + # "name": task["name"], + # "type": "task", + # "schema": "openpype:asset-3.0", + # "data": task_data, + # "parent": asset_doc["_id"], + # } + # insert_list.append(task_doc) # elif item.data(REMOVED_ROLE): # TODO removal # if item.data(HIERARCHY_CHANGE_ABLE_ROLE): From 9a1dd4fc0630f94cff1ef554fd31ede714b8ad49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Feb 2022 19:26:26 +0100 Subject: [PATCH 005/194] All bulk write. Updating assets --- .../default_modules/kitsu/kitsu_module.py | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 9730437ec2..1a71a05a7e 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -12,10 +12,8 @@ import click from avalon.api import AvalonMongoDB import gazu from openpype.lib import create_project -from openpype.lib.anatomy import Anatomy from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager - -# Import interface defined by this addon to be able find other addons using it +from pymongo import DeleteOne, InsertOne, UpdateOne from openpype_interfaces import IPluginPaths, ITrayAction @@ -183,9 +181,6 @@ def sync_local(): print(f"Creating project '{project_name}'") project_doc = create_project(project_name, project_code, dbcon=dbcon) - # Create project item - insert_list = [] - bulk_writes = [] for asset in all_assets: asset_data = {"zou_id": asset["id"]} @@ -196,34 +191,33 @@ def sync_local(): t["task_type_name"]: {"type": t["task_type_name"]} for t in asset_tasks } - # Create Asset - asset_doc = { - "name": asset["name"], - "type": "asset", - "schema": "openpype:asset-3.0", - "data": asset_data, - "parent": project_doc["_id"], - } + # Update or create asset + if asset["id"] in asset_docs_zou_ids: # Update asset + asset_doc = project_col.find_one({"data.zou_id": asset["id"]}) - if asset["id"] not in asset_docs_zou_ids: # Item is new - insert_list.append(asset_doc) - else: - asset_doc = project_col.find_one({"data": {"zou_id": asset["id"]}}) + # Override all 'data' TODO filter data to update? + diff_data = { + k: asset_data[k] + for k in asset_data.keys() + if asset_doc["data"].get(k) != asset_data[k] + } + if diff_data: + bulk_writes.append( + UpdateOne( + {"_id": asset_doc["_id"]}, {"$set": {"data": asset_data}} + ) + ) + else: # Create + asset_doc = { + "name": asset["name"], + "type": "asset", + "schema": "openpype:asset-3.0", + "data": asset_data, + "parent": project_doc["_id"], + } - # TODO update - # for task in asset_tasks: - # # print(task) - # task_data = {"zou_id": task["id"]} - - # # Create Task - # task_doc = { - # "name": task["name"], - # "type": "task", - # "schema": "openpype:asset-3.0", - # "data": task_data, - # "parent": asset_doc["_id"], - # } - # insert_list.append(task_doc) + # Insert new doc + bulk_writes.append(InsertOne(asset_doc)) # elif item.data(REMOVED_ROLE): # TODO removal # if item.data(HIERARCHY_CHANGE_ABLE_ROLE): @@ -244,13 +238,9 @@ def sync_local(): # update_data # )) - # Insert new docs if created - if insert_list: - project_col.insert_many(insert_list) - - # Write into DB TODO - # if bulk_writes: - # project_col.bulk_write(bulk_writes) + # Write into DB + if bulk_writes: + project_col.bulk_write(bulk_writes) dbcon.uninstall() From 206bf9f3c18873a6f81734841779eeab4f8829d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 9 Feb 2022 09:45:11 +0100 Subject: [PATCH 006/194] Delete assets --- .../default_modules/kitsu/kitsu_module.py | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 1a71a05a7e..1ce1bef6a2 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -170,10 +170,13 @@ def sync_local(): # Query all assets of the local project project_col = dbcon.database[project_code] - asset_docs_zou_ids = { - asset_doc["data"]["zou_id"] + asset_doc_ids = { + asset_doc["_id"]: asset_doc for asset_doc in project_col.find({"type": "asset"}) } + asset_docs_zou_ids = { + asset_doc["data"]["zou_id"] for asset_doc in asset_doc_ids.values() + } # Create project if is not available # - creation is required to be able set project anatomy and attributes @@ -182,6 +185,7 @@ def sync_local(): project_doc = create_project(project_name, project_code, dbcon=dbcon) bulk_writes = [] + sync_assets = set() for asset in all_assets: asset_data = {"zou_id": asset["id"]} @@ -195,13 +199,13 @@ def sync_local(): if asset["id"] in asset_docs_zou_ids: # Update asset asset_doc = project_col.find_one({"data.zou_id": asset["id"]}) - # Override all 'data' TODO filter data to update? - diff_data = { + # Override all 'data' + updated_data = { k: asset_data[k] for k in asset_data.keys() if asset_doc["data"].get(k) != asset_data[k] } - if diff_data: + if updated_data: bulk_writes.append( UpdateOne( {"_id": asset_doc["_id"]}, {"$set": {"data": asset_data}} @@ -219,24 +223,16 @@ def sync_local(): # Insert new doc bulk_writes.append(InsertOne(asset_doc)) - # elif item.data(REMOVED_ROLE): # TODO removal - # if item.data(HIERARCHY_CHANGE_ABLE_ROLE): - # bulk_writes.append(DeleteOne( - # {"_id": item.asset_id} - # )) - # else: - # bulk_writes.append(UpdateOne( - # {"_id": item.asset_id}, - # {"$set": {"type": "archived_asset"}} - # )) + # Keep synchronized asset for diff + sync_assets.add(asset_doc["_id"]) - # else: TODO update data - # update_data = new_item.update_data() - # if update_data: - # bulk_writes.append(UpdateOne( - # {"_id": new_item.asset_id}, - # update_data - # )) + # Delete from diff of assets in OP and synchronized assets to detect deleted assets + diff_assets = set(asset_doc_ids.keys()) - sync_assets + if diff_assets: + # Delete doc + bulk_writes.extend( + [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] + ) # Write into DB if bulk_writes: From e882de52f04d2ca79e30fb23801257cefd085e56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 10 Feb 2022 17:35:03 +0100 Subject: [PATCH 007/194] Assets hierarchy --- .../default_modules/kitsu/kitsu_module.py | 165 ++++++++++++------ 1 file changed, 114 insertions(+), 51 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 1ce1bef6a2..92d724be67 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -7,13 +7,15 @@ in global space here until are required or used. - we still support Python 2 hosts where addon definition should available """ import os +from typing import Dict, List, Set import click from avalon.api import AvalonMongoDB import gazu from openpype.lib import create_project from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager -from pymongo import DeleteOne, InsertOne, UpdateOne +from pymongo import DeleteOne, UpdateOne +from pymongo.collection import Collection from openpype_interfaces import IPluginPaths, ITrayAction @@ -144,8 +146,8 @@ def cli_main(): @cli_main.command() -def sync_local(): - """Synchronize local database from Zou sever database.""" +def sync_openpype(): + """Synchronize openpype database from Zou sever database.""" # Connect to server gazu.client.set_host(os.environ["KITSU_SERVER"]) @@ -167,69 +169,61 @@ def sync_local(): # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) - - # Query all assets of the local project - project_col = dbcon.database[project_code] - asset_doc_ids = { - asset_doc["_id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) - } - asset_docs_zou_ids = { - asset_doc["data"]["zou_id"] for asset_doc in asset_doc_ids.values() - } + all_episodes = gazu.shot.all_episodes_for_project(project) + all_seqs = gazu.shot.all_sequences_for_project(project) + all_shots = gazu.shot.all_shots_for_project(project) # Create project if is not available # - creation is required to be able set project anatomy and attributes + to_insert = [] if not project_doc: print(f"Creating project '{project_name}'") project_doc = create_project(project_name, project_code, dbcon=dbcon) - bulk_writes = [] - sync_assets = set() - for asset in all_assets: - asset_data = {"zou_id": asset["id"]} + # Query all assets of the local project + project_col = dbcon.database[project_code] + asset_doc_ids = { + asset_doc["data"]["zou_id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou_id") + } + asset_doc_ids[project["id"]] = project_doc - # Set tasks - asset_tasks = gazu.task.all_tasks_for_asset(asset) - asset_data["tasks"] = { - t["task_type_name"]: {"type": t["task_type_name"]} for t in asset_tasks - } - - # Update or create asset - if asset["id"] in asset_docs_zou_ids: # Update asset - asset_doc = project_col.find_one({"data.zou_id": asset["id"]}) - - # Override all 'data' - updated_data = { - k: asset_data[k] - for k in asset_data.keys() - if asset_doc["data"].get(k) != asset_data[k] - } - if updated_data: - bulk_writes.append( - UpdateOne( - {"_id": asset_doc["_id"]}, {"$set": {"data": asset_data}} - ) - ) - else: # Create - asset_doc = { - "name": asset["name"], + # Create + to_insert.extend( + [ + { + "name": item["name"], "type": "asset", "schema": "openpype:asset-3.0", - "data": asset_data, - "parent": project_doc["_id"], + "data": {"zou_id": item["id"], "tasks": {}}, } + for item in all_episodes + all_assets + all_seqs + all_shots + if item["id"] not in asset_doc_ids.keys() + ] + ) + if to_insert: + # Insert in doc + project_col.insert_many(to_insert) - # Insert new doc - bulk_writes.append(InsertOne(asset_doc)) + # Update existing docs + asset_doc_ids.update( + { + asset_doc["data"]["zou_id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou_id") + } + ) - # Keep synchronized asset for diff - sync_assets.add(asset_doc["_id"]) + # Update + all_entities = all_assets + all_episodes + all_seqs + all_shots + bulk_writes = update_op_assets(project_col, all_entities, asset_doc_ids) - # Delete from diff of assets in OP and synchronized assets to detect deleted assets - diff_assets = set(asset_doc_ids.keys()) - sync_assets + # Delete + diff_assets = set(asset_doc_ids.keys()) - { + e["id"] for e in all_entities + [project] + } if diff_assets: - # Delete doc bulk_writes.extend( [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] ) @@ -241,6 +235,75 @@ def sync_local(): dbcon.uninstall() +def update_op_assets( + project_col: Collection, items_list: List[dict], asset_doc_ids: Dict[str, dict] +) -> List[UpdateOne]: + """Update OpenPype assets. + Set 'data' and 'parent' fields. + + :param project_col: Project collection to query data from + :param items_list: List of zou items to update + :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] + :return: List of UpdateOne objects + """ + bulk_writes = [] + for item in items_list: + # Update asset + item_doc = project_col.find_one({"data.zou_id": item["id"]}) + item_data = item_doc["data"].copy() + + # Tasks + tasks_list = None + if item["type"] == "Asset": + tasks_list = gazu.task.all_tasks_for_asset(item) + elif item["type"] == "Shot": + tasks_list = gazu.task.all_tasks_for_shot(item) + if tasks_list: + item_data["tasks"] = { + t["task_type_name"]: {"type": t["task_type_name"]} for t in tasks_list + } + + # Visual parent for hierarchy + direct_parent_id = item["parent_id"] or item["source_id"] + if direct_parent_id: + visual_parent_doc = asset_doc_ids[direct_parent_id] + item_data["visualParent"] = visual_parent_doc["_id"] + + # Add parents for hierarchy + parent_zou_id = item["parent_id"] + item_data["parents"] = [] + while parent_zou_id is not None: + parent_doc = asset_doc_ids[parent_zou_id] + item_data["parents"].insert(0, parent_doc["name"]) + + parent_zou_id = next( + i for i in items_list if i["id"] == parent_doc["data"]["zou_id"] + )["parent_id"] + + # TODO create missing tasks before + + # Update 'data' different in zou DB + updated_data = { + k: item_data[k] + for k in item_data.keys() + if item_doc["data"].get(k) != item_data[k] + } + if updated_data or not item_doc.get("parent"): + bulk_writes.append( + UpdateOne( + {"_id": item_doc["_id"]}, + { + "$set": { + "data": item_data, + "parent": asset_doc_ids[item["project_id"]]["_id"], + } + }, + ) + ) + + return bulk_writes + + @cli_main.command() def show_dialog(): """Show ExampleAddon dialog. From ee281f740d58821ab4d5e190e6b4863af5c2ac44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 10 Feb 2022 18:03:27 +0100 Subject: [PATCH 008/194] Project tasks --- .../default_modules/kitsu/kitsu_module.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 92d724be67..84709bc0a2 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -160,6 +160,8 @@ def sync_openpype(): dbcon.install() all_projects = gazu.project.all_projects() for project in all_projects: + bulk_writes = [] + # Create project locally # Try to find project document project_name = project["name"] @@ -180,6 +182,23 @@ def sync_openpype(): print(f"Creating project '{project_name}'") project_doc = create_project(project_name, project_code, dbcon=dbcon) + # Project tasks + bulk_writes.append( + UpdateOne( + {"_id": project_doc["_id"]}, + { + "$set": { + "config.tasks": { + t["name"]: { + "short_name": t.get("short_name", t["name"]) + } + for t in gazu.task.all_task_types_for_project(project) + } + } + }, + ) + ) + # Query all assets of the local project project_col = dbcon.database[project_code] asset_doc_ids = { @@ -217,7 +236,7 @@ def sync_openpype(): # Update all_entities = all_assets + all_episodes + all_seqs + all_shots - bulk_writes = update_op_assets(project_col, all_entities, asset_doc_ids) + bulk_writes.extend(update_op_assets(project_col, all_entities, asset_doc_ids)) # Delete diff_assets = set(asset_doc_ids.keys()) - { @@ -228,7 +247,7 @@ def sync_openpype(): [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] ) - # Write into DB + # Write into DB # TODO make it common for all projects if bulk_writes: project_col.bulk_write(bulk_writes) From d63c5fcae8ec1ee8c49c4a21dea86b3e9da157d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 11 Feb 2022 09:29:08 +0100 Subject: [PATCH 009/194] Optim: bulkwrite and queries mutualization --- .../default_modules/kitsu/kitsu_module.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 84709bc0a2..89312a344c 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -159,9 +159,8 @@ def sync_openpype(): dbcon = AvalonMongoDB() dbcon.install() all_projects = gazu.project.all_projects() + bulk_writes = [] for project in all_projects: - bulk_writes = [] - # Create project locally # Try to find project document project_name = project["name"] @@ -236,7 +235,7 @@ def sync_openpype(): # Update all_entities = all_assets + all_episodes + all_seqs + all_shots - bulk_writes.extend(update_op_assets(project_col, all_entities, asset_doc_ids)) + bulk_writes.extend(update_op_assets(all_entities, asset_doc_ids)) # Delete diff_assets = set(asset_doc_ids.keys()) - { @@ -247,20 +246,19 @@ def sync_openpype(): [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] ) - # Write into DB # TODO make it common for all projects - if bulk_writes: - project_col.bulk_write(bulk_writes) + # Write into DB + if bulk_writes: + project_col.bulk_write(bulk_writes) dbcon.uninstall() def update_op_assets( - project_col: Collection, items_list: List[dict], asset_doc_ids: Dict[str, dict] + items_list: List[dict], asset_doc_ids: Dict[str, dict] ) -> List[UpdateOne]: """Update OpenPype assets. Set 'data' and 'parent' fields. - :param project_col: Project collection to query data from :param items_list: List of zou items to update :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] :return: List of UpdateOne objects @@ -268,7 +266,7 @@ def update_op_assets( bulk_writes = [] for item in items_list: # Update asset - item_doc = project_col.find_one({"data.zou_id": item["id"]}) + item_doc = asset_doc_ids[item["id"]] item_data = item_doc["data"].copy() # Tasks @@ -299,8 +297,6 @@ def update_op_assets( i for i in items_list if i["id"] == parent_doc["data"]["zou_id"] )["parent_id"] - # TODO create missing tasks before - # Update 'data' different in zou DB updated_data = { k: item_data[k] From 4e68bcf55fd2552f53904dcd0a6c47b56946c927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 11 Feb 2022 09:31:14 +0100 Subject: [PATCH 010/194] cleaning --- openpype/modules/default_modules/kitsu/kitsu_module.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 89312a344c..e52d18b84b 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -15,7 +15,6 @@ import gazu from openpype.lib import create_project from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager from pymongo import DeleteOne, UpdateOne -from pymongo.collection import Collection from openpype_interfaces import IPluginPaths, ITrayAction From 294b93f65a97d17f503a82962f121c3202002a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Sat, 12 Feb 2022 15:06:22 +0100 Subject: [PATCH 011/194] Create episode --- .../default_modules/kitsu/kitsu_module.py | 226 +++++++++++++++++- .../defaults/project_settings/kitsu.json | 6 +- .../projects_schema/schema_project_kitsu.json | 26 +- 3 files changed, 243 insertions(+), 15 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index e52d18b84b..c4f627d5ad 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -6,12 +6,14 @@ in global space here until are required or used. - imports of Python 3 packages - we still support Python 2 hosts where addon definition should available """ -import os -from typing import Dict, List, Set import click +import os +import re +from typing import Dict, List from avalon.api import AvalonMongoDB import gazu +from openpype.api import get_project_settings from openpype.lib import create_project from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager from pymongo import DeleteOne, UpdateOne @@ -145,8 +147,8 @@ def cli_main(): @cli_main.command() -def sync_openpype(): - """Synchronize openpype database from Zou sever database.""" +def sync_zou(): + """Synchronize Zou server database (Kitsu backend) with openpype database.""" # Connect to server gazu.client.set_host(os.environ["KITSU_SERVER"]) @@ -157,8 +159,108 @@ def sync_openpype(): # Iterate projects dbcon = AvalonMongoDB() dbcon.install() - all_projects = gazu.project.all_projects() + + op_projects = [p for p in dbcon.projects()] bulk_writes = [] + for op_project in op_projects: + # Create project locally + # Try to find project document + project_name = op_project["name"] + project_code = op_project["data"]["code"] + dbcon.Session["AVALON_PROJECT"] = project_name + + # Get all entities from zou + zou_project = gazu.project.get_project_by_name(project_name) + + # Create project + if zou_project is None: + raise RuntimeError( + f"Project '{project_name}' doesn't exist in Zou database, please create it in Kitsu and add logged user to it before running synchronization." + ) + + # Update project settings and data + zou_project.update( + { + "code": op_project["data"]["code"], + "fps": op_project["data"]["fps"], + "resolution": f"{op_project['data']['resolutionWidth']}x{op_project['data']['resolutionHeight']}", + } + ) + gazu.project.update_project(zou_project) + gazu.project.update_project_data(zou_project, data=op_project["data"]) + + all_assets = gazu.asset.all_assets_for_project(zou_project) + all_episodes = gazu.shot.all_episodes_for_project(zou_project) + all_seqs = gazu.shot.all_sequences_for_project(zou_project) + all_shots = gazu.shot.all_shots_for_project(zou_project) + print(zou_project["name"]) + all_entities_ids = { + e["id"] for e in all_episodes + all_seqs + all_shots + all_assets + } + + project_module_settings = get_project_settings(project_name)["kitsu"] + + # Create new assets + # Query all assets of the local project + project_col = dbcon.database[project_name] + asset_docs = [asset_doc for asset_doc in project_col.find({"type": "asset"})] + + new_assets_docs = [ + doc + for doc in asset_docs + if doc["data"].get("zou_id") not in all_entities_ids + ] + naming_pattern = project_module_settings["entities_naming_pattern"] + regex_ep = re.compile( + r"({})|({})|({})".format( + naming_pattern["episode"].replace("#", "\d"), + naming_pattern["sequence"].replace("#", "\d"), + naming_pattern["shot"].replace("#", "\d"), + ), + re.IGNORECASE, + ) + for doc in new_assets_docs: + match = regex_ep.match(doc["name"]) + if not match: + # TODO asset + continue + + print(doc) + if match.group(1): # Episode + new_episode = gazu.shot.new_episode(zou_project, doc["name"]) + + # Update doc with zou id + bulk_writes.append( + UpdateOne( + {"_id": doc["_id"]}, + {"$set": {"data.zou_id": new_episode["id"]}}, + ) + ) + elif match.group(2): # Sequence + # TODO match zou episode + new_sequence = gazu.shot.new_sequence(zou_project, doc["name"]) + + # Update doc with zou id + bulk_writes.append( + UpdateOne( + {"_id": doc["_id"]}, + {"$set": {"data.zou_id": new_sequence["id"]}}, + ) + ) + elif match.group(3): # Shot + pass + + # Delete + # if gazu. + + # Write into DB + if bulk_writes: + project_col.bulk_write(bulk_writes) + + dbcon.uninstall() + + return + for project in all_projects: # Create project locally # Try to find project document @@ -245,9 +347,117 @@ def sync_openpype(): [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] ) - # Write into DB - if bulk_writes: - project_col.bulk_write(bulk_writes) + +@cli_main.command() +def sync_openpype(): + """Synchronize openpype database from Zou sever database.""" + + # Connect to server + gazu.client.set_host(os.environ["KITSU_SERVER"]) + + # Authenticate + gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) + + # Iterate projects + dbcon = AvalonMongoDB() + dbcon.install() + all_projects = gazu.project.all_projects() + bulk_writes = [] + for project in all_projects: + # Create project locally + # Try to find project document + project_name = project["name"] + project_code = project_name + dbcon.Session["AVALON_PROJECT"] = project_name + project_doc = dbcon.find_one({"type": "project"}) + + # Get all assets from zou + all_assets = gazu.asset.all_assets_for_project(project) + all_episodes = gazu.shot.all_episodes_for_project(project) + all_seqs = gazu.shot.all_sequences_for_project(project) + all_shots = gazu.shot.all_shots_for_project(project) + + # Create project if is not available + # - creation is required to be able set project anatomy and attributes + to_insert = [] + if not project_doc: + print(f"Creating project '{project_name}'") + project_doc = create_project(project_name, project_code, dbcon=dbcon) + + # Project data and tasks + bulk_writes.append( + UpdateOne( + {"_id": project_doc["_id"]}, + { + "$set": { + "config.tasks": { + t["name"]: {"short_name": t.get("short_name", t["name"])} + for t in gazu.task.all_task_types_for_project(project) + }, + "data": project["data"].update( + { + "code": project["code"], + "fps": project_code["fps"], + "resolutionWidth": project["resolution"].split("x")[0], + "resolutionHeight": project["resolution"].split("x")[1], + } + ), + } + }, + ) + ) + + # Query all assets of the local project + project_col = dbcon.database[project_code] + asset_doc_ids = { + asset_doc["data"]["zou_id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou_id") + } + asset_doc_ids[project["id"]] = project_doc + + # Create + to_insert.extend( + [ + { + "name": item["name"], + "type": "asset", + "schema": "openpype:asset-3.0", + "data": {"zou_id": item["id"], "tasks": {}}, + } + for item in all_episodes + all_assets + all_seqs + all_shots + if item["id"] not in asset_doc_ids.keys() + ] + ) + if to_insert: + # Insert in doc + project_col.insert_many(to_insert) + + # Update existing docs + asset_doc_ids.update( + { + asset_doc["data"]["zou_id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou_id") + } + ) + + # Update + all_entities = all_assets + all_episodes + all_seqs + all_shots + bulk_writes.extend(update_op_assets(all_entities, asset_doc_ids)) + + # Delete + diff_assets = set(asset_doc_ids.keys()) - { + e["id"] for e in all_entities + [project] + } + if diff_assets: + bulk_writes.extend( + [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] + ) + + # Write into DB + if bulk_writes: + project_col.bulk_write(bulk_writes) dbcon.uninstall() diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index b4d2ccc611..435814a9d1 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -1,3 +1,7 @@ { - "number": 0 + "entities_naming_pattern": { + "episode": "E##", + "sequence": "SQ##", + "shot": "SH##" + } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 93976cc03b..a504959001 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -6,12 +6,26 @@ "is_file": true, "children": [ { - "type": "number", - "key": "number", - "label": "This is your lucky number:", - "minimum": 7, - "maximum": 7, - "decimals": 0 + "type": "dict", + "key": "entities_naming_pattern", + "label": "Entities naming pattern", + "children": [ + { + "type": "text", + "key": "episode", + "label": "Episode:" + }, + { + "type": "text", + "key": "sequence", + "label": "Sequence:" + }, + { + "type": "text", + "key": "shot", + "label": "Shot:" + } + ] } ] } From bad16651e1b2ed2d307496941ac95adb1224040a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Mon, 14 Feb 2022 17:58:06 +0100 Subject: [PATCH 012/194] Create asset, ep, seq and shot in zou --- .../default_modules/kitsu/kitsu_module.py | 123 +++++++++++++----- 1 file changed, 93 insertions(+), 30 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index c4f627d5ad..453d1c5315 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -189,6 +189,7 @@ def sync_zou(): gazu.project.update_project(zou_project) gazu.project.update_project_data(zou_project, data=op_project["data"]) + all_asset_types = gazu.asset.all_asset_types() all_assets = gazu.asset.all_assets_for_project(zou_project) all_episodes = gazu.shot.all_episodes_for_project(zou_project) all_seqs = gazu.shot.all_sequences_for_project(zou_project) @@ -199,15 +200,17 @@ def sync_zou(): } project_module_settings = get_project_settings(project_name)["kitsu"] + project_col = dbcon.database[project_name] + asset_docs = { + asset_doc["_id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + } # Create new assets # Query all assets of the local project - project_col = dbcon.database[project_name] - asset_docs = [asset_doc for asset_doc in project_col.find({"type": "asset"})] - new_assets_docs = [ doc - for doc in asset_docs + for doc in asset_docs.values() if doc["data"].get("zou_id") not in all_entities_ids ] naming_pattern = project_module_settings["entities_naming_pattern"] @@ -220,37 +223,95 @@ def sync_zou(): re.IGNORECASE, ) for doc in new_assets_docs: + visual_parent_id = doc["data"]["visualParent"] + match = regex_ep.match(doc["name"]) - if not match: - # TODO asset - continue - - print(doc) - if match.group(1): # Episode - new_episode = gazu.shot.new_episode(zou_project, doc["name"]) - - # Update doc with zou id - bulk_writes.append( - UpdateOne( - {"_id": doc["_id"]}, - {"$set": {"data.zou_id": new_episode["id"]}}, - ) + if not match: # Asset + new_entity = gazu.asset.new_asset( + zou_project, all_asset_types[0], doc["name"] ) + + elif match.group(1): # Episode + new_entity = gazu.shot.new_episode(zou_project, doc["name"]) + elif match.group(2): # Sequence - # TODO match zou episode - new_sequence = gazu.shot.new_sequence(zou_project, doc["name"]) - - # Update doc with zou id - bulk_writes.append( - UpdateOne( - {"_id": doc["_id"]}, - {"$set": {"data.zou_id": new_sequence["id"]}}, - ) + parent_doc = asset_docs[visual_parent_id] + new_entity = gazu.shot.new_sequence( + zou_project, doc["name"], episode=parent_doc["data"]["zou_id"] ) - elif match.group(3): # Shot - pass - # Delete + elif match.group(3): # Shot + # Match and check parent doc + parent_doc = asset_docs[visual_parent_id] + zou_parent_id = parent_doc["data"]["zou_id"] + if parent_doc["data"].get("zou", {}).get("type") != "Sequence": + # Warn + print( + f"Shot {doc['name']} must be parented to a Sequence in Kitsu. Creating automatically one substitute sequence..." + ) + + # Create new sequence + digits_padding = naming_pattern["sequence"].count("#") + substitute_sequence_name = f'{naming_pattern["sequence"].replace("#" * digits_padding, "1".zfill(digits_padding))}' + new_sequence = gazu.shot.new_sequence( + zou_project, substitute_sequence_name, episode=zou_parent_id + ) + + # Insert doc + inserted = project_col.insert_one( + { + "name": substitute_sequence_name, + "type": "asset", + "schema": "openpype:asset-3.0", + "data": { + "zou_id": new_sequence["id"], + "tasks": {}, + "parents": parent_doc["data"]["parents"] + + [parent_doc["name"]], + "visualParent": parent_doc["_id"], + }, + "parent": parent_doc["_id"], + } + ) + visual_parent_id = inserted.inserted_id + + # Update parent ID + zou_parent_id = new_sequence["id"] + + # Create shot + new_entity = gazu.shot.new_shot( + zou_project, + zou_parent_id, + doc["name"], + frame_in=doc["data"]["frameStart"], + frame_out=doc["data"]["frameEnd"], + nb_frames=doc["data"]["frameEnd"] - doc["data"]["frameStart"], + ) + + # Update doc with zou id + doc["data"].update( + { + "zou_id": new_entity["id"], + "visualParent": visual_parent_id, + "zou": new_entity, + } + ) + bulk_writes.append( + UpdateOne( + {"_id": doc["_id"]}, + { + "$set": { + "data.zou_id": new_entity["id"], + "data.visualParent": visual_parent_id, + "data.zou": new_entity, + } + }, + ) + ) + + # TODO update / tasks + + # Delete TODO # if gazu. # Write into DB @@ -477,6 +538,7 @@ def update_op_assets( # Update asset item_doc = asset_doc_ids[item["id"]] item_data = item_doc["data"].copy() + item_data["zou"] = item # Tasks tasks_list = None @@ -484,6 +546,7 @@ def update_op_assets( tasks_list = gazu.task.all_tasks_for_asset(item) elif item["type"] == "Shot": tasks_list = gazu.task.all_tasks_for_shot(item) + # TODO frame in and out if tasks_list: item_data["tasks"] = { t["task_type_name"]: {"type": t["task_type_name"]} for t in tasks_list From 9af6264d1e02f92244d678964da5c5902428016f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 15 Feb 2022 18:21:26 +0100 Subject: [PATCH 013/194] Update and delete in zou --- .../default_modules/kitsu/kitsu_module.py | 146 +++++++----------- 1 file changed, 53 insertions(+), 93 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 453d1c5315..e42edbc52b 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -6,6 +6,8 @@ in global space here until are required or used. - imports of Python 3 packages - we still support Python 2 hosts where addon definition should available """ +from asyncio import all_tasks +from turtle import update import click import os import re @@ -170,6 +172,7 @@ def sync_zou(): dbcon.Session["AVALON_PROJECT"] = project_name # Get all entities from zou + print(f"Synchronizing {project_name}...") zou_project = gazu.project.get_project_by_name(project_name) # Create project @@ -194,11 +197,11 @@ def sync_zou(): all_episodes = gazu.shot.all_episodes_for_project(zou_project) all_seqs = gazu.shot.all_sequences_for_project(zou_project) all_shots = gazu.shot.all_shots_for_project(zou_project) - print(zou_project["name"]) all_entities_ids = { e["id"] for e in all_episodes + all_seqs + all_shots + all_assets } + # Query all assets of the local project project_module_settings = get_project_settings(project_name)["kitsu"] project_col = dbcon.database[project_name] asset_docs = { @@ -207,7 +210,6 @@ def sync_zou(): } # Create new assets - # Query all assets of the local project new_assets_docs = [ doc for doc in asset_docs.values() @@ -225,6 +227,7 @@ def sync_zou(): for doc in new_assets_docs: visual_parent_id = doc["data"]["visualParent"] + # Match asset type by it's name match = regex_ep.match(doc["name"]) if not match: # Asset new_entity = gazu.asset.new_asset( @@ -269,6 +272,7 @@ def sync_zou(): "parents": parent_doc["data"]["parents"] + [parent_doc["name"]], "visualParent": parent_doc["_id"], + "zou": new_sequence, }, "parent": parent_doc["_id"], } @@ -309,10 +313,54 @@ def sync_zou(): ) ) - # TODO update / tasks + # Update assets + all_tasks_types = {t["name"]: t for t in gazu.task.all_task_types()} + assets_docs_to_update = [ + doc + for doc in asset_docs.values() + if doc["data"].get("zou_id") in all_entities_ids + ] + for doc in assets_docs_to_update: + zou_id = doc["data"].get("zou", {}).get("id") + if zou_id: + # Data + entity_data = {} + frame_in = doc["data"].get("frameStart") + frame_out = doc["data"].get("frameEnd") + if frame_in or frame_out: + entity_data.update( + { + "data": {"frame_in": frame_in, "frame_out": frame_out}, + "nb_frames": frame_out - frame_in, + } + ) + entity = gazu.raw.update("entities", zou_id, entity_data) - # Delete TODO - # if gazu. + # Tasks + all_tasks_func = getattr( + gazu.task, f"all_tasks_for_{entity['type'].lower()}" + ) + entity_tasks = {t["name"] for t in all_tasks_func(entity)} + for task_name in doc["data"]["tasks"].keys(): + # Create only if new + if task_name not in entity_tasks: + task_type = all_tasks_types.get(task_name) + + # Create non existing task + if not task_type: + task_type = gazu.task.new_task_type(task_name) + all_tasks_types[task_name] = task_type + + # New task for entity + gazu.task.new_task(entity, task_type) + + # Delete + deleted_entities = all_entities_ids - { + asset_doc["data"].get("zou", {}).get("id") + for asset_doc in asset_docs.values() + } + for entity_id in deleted_entities: + gazu.raw.delete(f"data/entities/{entity_id}") # Write into DB if bulk_writes: @@ -320,94 +368,6 @@ def sync_zou(): dbcon.uninstall() - return - - for project in all_projects: - # Create project locally - # Try to find project document - project_name = project["name"] - project_code = project_name - dbcon.Session["AVALON_PROJECT"] = project_name - project_doc = dbcon.find_one({"type": "project"}) - - # Get all assets from zou - all_assets = gazu.asset.all_assets_for_project(project) - all_episodes = gazu.shot.all_episodes_for_project(project) - all_seqs = gazu.shot.all_sequences_for_project(project) - all_shots = gazu.shot.all_shots_for_project(project) - - # Create project if is not available - # - creation is required to be able set project anatomy and attributes - to_insert = [] - if not project_doc: - print(f"Creating project '{project_name}'") - project_doc = create_project(project_name, project_code, dbcon=dbcon) - - # Project tasks - bulk_writes.append( - UpdateOne( - {"_id": project_doc["_id"]}, - { - "$set": { - "config.tasks": { - t["name"]: { - "short_name": t.get("short_name", t["name"]) - } - for t in gazu.task.all_task_types_for_project(project) - } - } - }, - ) - ) - - # Query all assets of the local project - project_col = dbcon.database[project_code] - asset_doc_ids = { - asset_doc["data"]["zou_id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) - if asset_doc["data"].get("zou_id") - } - asset_doc_ids[project["id"]] = project_doc - - # Create - to_insert.extend( - [ - { - "name": item["name"], - "type": "asset", - "schema": "openpype:asset-3.0", - "data": {"zou_id": item["id"], "tasks": {}}, - } - for item in all_episodes + all_assets + all_seqs + all_shots - if item["id"] not in asset_doc_ids.keys() - ] - ) - if to_insert: - # Insert in doc - project_col.insert_many(to_insert) - - # Update existing docs - asset_doc_ids.update( - { - asset_doc["data"]["zou_id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) - if asset_doc["data"].get("zou_id") - } - ) - - # Update - all_entities = all_assets + all_episodes + all_seqs + all_shots - bulk_writes.extend(update_op_assets(all_entities, asset_doc_ids)) - - # Delete - diff_assets = set(asset_doc_ids.keys()) - { - e["id"] for e in all_entities + [project] - } - if diff_assets: - bulk_writes.extend( - [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] - ) - @cli_main.command() def sync_openpype(): From b2ce0ac07a04ebdc2e61b4289ec2c2fcde624c63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 15 Feb 2022 18:29:43 +0100 Subject: [PATCH 014/194] Shot naming matching --- .../default_modules/kitsu/kitsu_module.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index e42edbc52b..a67f0e2d58 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -233,16 +233,7 @@ def sync_zou(): new_entity = gazu.asset.new_asset( zou_project, all_asset_types[0], doc["name"] ) - - elif match.group(1): # Episode - new_entity = gazu.shot.new_episode(zou_project, doc["name"]) - - elif match.group(2): # Sequence - parent_doc = asset_docs[visual_parent_id] - new_entity = gazu.shot.new_sequence( - zou_project, doc["name"], episode=parent_doc["data"]["zou_id"] - ) - + # Match case in shot Date: Tue, 15 Feb 2022 18:30:06 +0100 Subject: [PATCH 015/194] cleaning --- openpype/modules/default_modules/kitsu/kitsu_module.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index a67f0e2d58..47af25ce60 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -6,8 +6,6 @@ in global space here until are required or used. - imports of Python 3 packages - we still support Python 2 hosts where addon definition should available """ -from asyncio import all_tasks -from turtle import update import click import os import re From a8611c07a8d56942950a2294a95f06f07aa87d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 16 Feb 2022 15:42:23 +0100 Subject: [PATCH 016/194] Clean sync zou --- .../default_modules/kitsu/kitsu_module.py | 138 +++++++++--------- 1 file changed, 72 insertions(+), 66 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 47af25ce60..ce4c579607 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -13,6 +13,24 @@ from typing import Dict, List from avalon.api import AvalonMongoDB import gazu +from gazu.asset import all_assets_for_project, all_asset_types, new_asset +from gazu.shot import ( + all_episodes_for_project, + all_sequences_for_project, + all_shots_for_project, + new_episode, + new_sequence, + new_shot, + update_sequence, +) +from gazu.task import ( + all_tasks_for_asset, + all_tasks_for_shot, + all_task_types, + all_task_types_for_project, + new_task, + new_task_type, +) from openpype.api import get_project_settings from openpype.lib import create_project from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager @@ -176,7 +194,7 @@ def sync_zou(): # Create project if zou_project is None: raise RuntimeError( - f"Project '{project_name}' doesn't exist in Zou database, please create it in Kitsu and add logged user to it before running synchronization." + f"Project '{project_name}' doesn't exist in Zou database, please create it in Kitsu and add OpenPype user to it before running synchronization." ) # Update project settings and data @@ -190,11 +208,11 @@ def sync_zou(): gazu.project.update_project(zou_project) gazu.project.update_project_data(zou_project, data=op_project["data"]) - all_asset_types = gazu.asset.all_asset_types() - all_assets = gazu.asset.all_assets_for_project(zou_project) - all_episodes = gazu.shot.all_episodes_for_project(zou_project) - all_seqs = gazu.shot.all_sequences_for_project(zou_project) - all_shots = gazu.shot.all_shots_for_project(zou_project) + asset_types = all_asset_types() + all_assets = all_assets_for_project(zou_project) + all_episodes = all_episodes_for_project(zou_project) + all_seqs = all_sequences_for_project(zou_project) + all_shots = all_shots_for_project(zou_project) all_entities_ids = { e["id"] for e in all_episodes + all_seqs + all_shots + all_assets } @@ -211,14 +229,14 @@ def sync_zou(): new_assets_docs = [ doc for doc in asset_docs.values() - if doc["data"].get("zou_id") not in all_entities_ids + if doc["data"].get("zou", {}).get("id") not in all_entities_ids ] naming_pattern = project_module_settings["entities_naming_pattern"] regex_ep = re.compile( - r"({})|({})|({})".format( - naming_pattern["episode"].replace("#", "\d"), - naming_pattern["sequence"].replace("#", "\d"), - naming_pattern["shot"].replace("#", "\d"), + r"(.*{}.*)|(.*{}.*)|(.*{}.*)".format( + naming_pattern["shot"].replace("#", ""), + naming_pattern["sequence"].replace("#", ""), + naming_pattern["episode"].replace("#", ""), ), re.IGNORECASE, ) @@ -228,51 +246,38 @@ def sync_zou(): # Match asset type by it's name match = regex_ep.match(doc["name"]) if not match: # Asset - new_entity = gazu.asset.new_asset( - zou_project, all_asset_types[0], doc["name"] - ) + new_entity = new_asset(zou_project, asset_types[0], doc["name"]) # Match case in shot Date: Wed, 16 Feb 2022 17:30:35 +0100 Subject: [PATCH 017/194] Fix sync --- .../default_modules/kitsu/kitsu_module.py | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index ce4c579607..266aaa9139 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -184,7 +184,6 @@ def sync_zou(): # Create project locally # Try to find project document project_name = op_project["name"] - project_code = op_project["data"]["code"] dbcon.Session["AVALON_PROJECT"] = project_name # Get all entities from zou @@ -198,15 +197,16 @@ def sync_zou(): ) # Update project settings and data - zou_project.update( - { - "code": op_project["data"]["code"], - "fps": op_project["data"]["fps"], - "resolution": f"{op_project['data']['resolutionWidth']}x{op_project['data']['resolutionHeight']}", - } - ) + if op_project["data"]: + zou_project.update( + { + "code": op_project["data"]["code"], + "fps": op_project["data"]["fps"], + "resolution": f"{op_project['data']['resolutionWidth']}x{op_project['data']['resolutionHeight']}", + } + ) + gazu.project.update_project_data(zou_project, data=op_project["data"]) gazu.project.update_project(zou_project) - gazu.project.update_project_data(zou_project, data=op_project["data"]) asset_types = all_asset_types() all_assets = all_assets_for_project(zou_project) @@ -270,8 +270,9 @@ def sync_zou(): created_sequence = new_sequence( zou_project, substitute_sequence_name, episode=zou_parent_id ) - created_sequence["is_substitute"] = True - update_sequence(created_sequence) + gazu.shot.update_sequence_data( + created_sequence, {"is_substitute": True} + ) # Update parent ID zou_parent_id = created_sequence["id"] @@ -395,11 +396,18 @@ def sync_openpype(): dbcon.Session["AVALON_PROJECT"] = project_name project_doc = dbcon.find_one({"type": "project"}) + print(f"Synchronizing {project_name}...") + # Get all assets from zou all_assets = all_assets_for_project(project) all_episodes = all_episodes_for_project(project) all_seqs = all_sequences_for_project(project) all_shots = all_shots_for_project(project) + all_entities = [ + e + for e in all_assets + all_episodes + all_seqs + all_shots + if not e["data"].get("is_substitute") + ] # Create project if is not available # - creation is required to be able set project anatomy and attributes @@ -421,7 +429,7 @@ def sync_openpype(): "data": project["data"].update( { "code": project["code"], - "fps": project_code["fps"], + "fps": project["fps"], "resolutionWidth": project["resolution"].split("x")[0], "resolutionHeight": project["resolution"].split("x")[1], } @@ -449,9 +457,8 @@ def sync_openpype(): "schema": "openpype:asset-3.0", "data": {"zou": item, "tasks": {}}, } - for item in all_episodes + all_assets + all_seqs + all_shots + for item in all_entities if item["id"] not in asset_doc_ids.keys() - and not item.get("is_substitute") ] ) if to_insert: @@ -468,7 +475,6 @@ def sync_openpype(): ) # Update - all_entities = all_assets + all_episodes + all_seqs + all_shots bulk_writes.extend(update_op_assets(all_entities, asset_doc_ids)) # Delete From 93fb1ac0f1331439852104189733671386366861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 17 Feb 2022 11:12:13 +0100 Subject: [PATCH 018/194] Use substitutes id --- .../default_modules/kitsu/kitsu_module.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 266aaa9139..0529d38a0e 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -242,6 +242,7 @@ def sync_zou(): ) for doc in new_assets_docs: visual_parent_id = doc["data"]["visualParent"] + parent_substitutes = [] # Match asset type by it's name match = regex_ep.match(doc["name"]) @@ -273,6 +274,7 @@ def sync_zou(): gazu.shot.update_sequence_data( created_sequence, {"is_substitute": True} ) + parent_substitutes.append(created_sequence) # Update parent ID zou_parent_id = created_sequence["id"] @@ -310,6 +312,7 @@ def sync_zou(): "$set": { "data.visualParent": visual_parent_id, "data.zou": new_entity, + "data.parent_substitutes": parent_substitutes, } }, ) @@ -494,17 +497,17 @@ def sync_openpype(): def update_op_assets( - items_list: List[dict], asset_doc_ids: Dict[str, dict] + entities_list: List[dict], asset_doc_ids: Dict[str, dict] ) -> List[UpdateOne]: """Update OpenPype assets. Set 'data' and 'parent' fields. - :param items_list: List of zou items to update + :param entities_list: List of zou entities to update :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] :return: List of UpdateOne objects """ bulk_writes = [] - for item in items_list: + for item in entities_list: # Update asset item_doc = asset_doc_ids[item["id"]] item_data = item_doc["data"].copy() @@ -523,20 +526,28 @@ def update_op_assets( } # Visual parent for hierarchy - direct_parent_id = item["parent_id"] or item["source_id"] - if direct_parent_id: - visual_parent_doc = asset_doc_ids[direct_parent_id] + substitute_parent_item = ( + item_data["parent_substitutes"][0] + if item_data.get("parent_substitutes") + else None + ) + parent_zou_id = item["parent_id"] or item["source_id"] + if substitute_parent_item: + parent_zou_id = ( + substitute_parent_item["parent_id"] + or substitute_parent_item["source_id"] + ) + visual_parent_doc = asset_doc_ids[parent_zou_id] item_data["visualParent"] = visual_parent_doc["_id"] # Add parents for hierarchy - parent_zou_id = item["parent_id"] item_data["parents"] = [] while parent_zou_id is not None: parent_doc = asset_doc_ids[parent_zou_id] item_data["parents"].insert(0, parent_doc["name"]) parent_zou_id = next( - i for i in items_list if i["id"] == parent_doc["data"]["zou"]["id"] + i for i in entities_list if i["id"] == parent_doc["data"]["zou"]["id"] )["parent_id"] # Update 'data' different in zou DB From c058ba54c2763b4563f5912f8a8a8930cceafc1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 22 Feb 2022 18:27:06 +0100 Subject: [PATCH 019/194] Project sync and code DRYing --- .../modules/default_modules/kitsu/__init__.py | 10 +-- .../default_modules/kitsu/kitsu_module.py | 52 ++++++---------- .../default_modules/kitsu/listeners.py | 61 +++++++++++++++++++ .../default_modules/kitsu/utils/__init__.py | 0 .../default_modules/kitsu/utils/openpype.py | 47 ++++++++++++++ 5 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 openpype/modules/default_modules/kitsu/listeners.py create mode 100644 openpype/modules/default_modules/kitsu/utils/__init__.py create mode 100644 openpype/modules/default_modules/kitsu/utils/openpype.py diff --git a/openpype/modules/default_modules/kitsu/__init__.py b/openpype/modules/default_modules/kitsu/__init__.py index cd0c2ea8af..dc7c2dad50 100644 --- a/openpype/modules/default_modules/kitsu/__init__.py +++ b/openpype/modules/default_modules/kitsu/__init__.py @@ -4,12 +4,6 @@ If addon class or settings definition won't be here their definition won't be found by OpenPype discovery. """ -from .kitsu_module import ( - AddonSettingsDef, - KitsuModule -) +from .kitsu_module import AddonSettingsDef, KitsuModule -__all__ = ( - "AddonSettingsDef", - "KitsuModule" -) +__all__ = ("AddonSettingsDef", "KitsuModule") diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 0529d38a0e..3fddc48ee9 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -11,7 +11,6 @@ import os import re from typing import Dict, List -from avalon.api import AvalonMongoDB import gazu from gazu.asset import all_assets_for_project, all_asset_types, new_asset from gazu.shot import ( @@ -21,7 +20,6 @@ from gazu.shot import ( new_episode, new_sequence, new_shot, - update_sequence, ) from gazu.task import ( all_tasks_for_asset, @@ -31,11 +29,15 @@ from gazu.task import ( new_task, new_task_type, ) +from pymongo import DeleteOne, UpdateOne + +from avalon.api import AvalonMongoDB from openpype.api import get_project_settings from openpype.lib import create_project from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager -from pymongo import DeleteOne, UpdateOne +from openpype.modules.default_modules.kitsu.utils.openpype import sync_project from openpype_interfaces import IPluginPaths, ITrayAction +from .listeners import add_listeners # Settings definition of this addon using `JsonFilesSettingsDef` @@ -371,8 +373,6 @@ def sync_zou(): if bulk_writes: project_col.bulk_write(bulk_writes) - # TODO Create events daemons - dbcon.uninstall() @@ -412,35 +412,8 @@ def sync_openpype(): if not e["data"].get("is_substitute") ] - # Create project if is not available - # - creation is required to be able set project anatomy and attributes - to_insert = [] - if not project_doc: - print(f"Creating project '{project_name}'") - project_doc = create_project(project_name, project_code, dbcon=dbcon) - - # Project data and tasks - bulk_writes.append( - UpdateOne( - {"_id": project_doc["_id"]}, - { - "$set": { - "config.tasks": { - t["name"]: {"short_name": t.get("short_name", t["name"])} - for t in all_task_types_for_project(project) - }, - "data": project["data"].update( - { - "code": project["code"], - "fps": project["fps"], - "resolutionWidth": project["resolution"].split("x")[0], - "resolutionHeight": project["resolution"].split("x")[1], - } - ), - } - }, - ) - ) + # Sync project. Create if doesn't exist + bulk_writes.append(sync_project(project, dbcon)) # Query all assets of the local project project_col = dbcon.database[project_code] @@ -452,6 +425,7 @@ def sync_openpype(): asset_doc_ids[project["id"]] = project_doc # Create + to_insert = [] to_insert.extend( [ { @@ -572,6 +546,16 @@ def update_op_assets( return bulk_writes +@cli_main.command() +def listen(): + """Show ExampleAddon dialog. + + We don't have access to addon directly through cli so we have to create + it again. + """ + add_listeners() + + @cli_main.command() def show_dialog(): """Show ExampleAddon dialog. diff --git a/openpype/modules/default_modules/kitsu/listeners.py b/openpype/modules/default_modules/kitsu/listeners.py new file mode 100644 index 0000000000..5303933bce --- /dev/null +++ b/openpype/modules/default_modules/kitsu/listeners.py @@ -0,0 +1,61 @@ +from turtle import update +import gazu +import os + +from pymongo import DeleteOne, UpdateOne + +from avalon.api import AvalonMongoDB +from openpype.modules.default_modules.kitsu.utils.openpype import sync_project + + +def add_listeners(): + # Connect to server + gazu.client.set_host(os.environ["KITSU_SERVER"]) + + # Authenticate + gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) + gazu.set_event_host(os.environ["KITSU_SERVER"].replace("api", "socket.io")) + + # Connect to DB + dbcon = AvalonMongoDB() + dbcon.install() + + def new_project(data): + """Create new project into DB.""" + + # Use update process to avoid duplicating code + update_project(data) + + def update_project(data): + """Update project into DB.""" + # Get project entity + project = gazu.project.get_project(data["project_id"]) + project_name = project["name"] + dbcon.Session["AVALON_PROJECT"] = project_name + + update_project = sync_project(project, dbcon) + + # Write into DB + if update_project: + project_col = dbcon.database[project_name] + project_col.bulk_write([update_project]) + + def delete_project(data): + # Get project entity + print(data) # TODO check bugfix + project = gazu.project.get_project(data["project_id"]) + + # Delete project collection + project_col = dbcon.database[project["name"]] + project_col.drop() + + def new_asset(data): + print("Asset created %s" % data) + + event_client = gazu.events.init() + gazu.events.add_listener(event_client, "project:new", new_project) + gazu.events.add_listener(event_client, "project:update", update_project) + gazu.events.add_listener(event_client, "project:delete", delete_project) + gazu.events.add_listener(event_client, "asset:new", new_asset) + gazu.events.run_client(event_client) + print("ll") diff --git a/openpype/modules/default_modules/kitsu/utils/__init__.py b/openpype/modules/default_modules/kitsu/utils/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/modules/default_modules/kitsu/utils/openpype.py b/openpype/modules/default_modules/kitsu/utils/openpype.py new file mode 100644 index 0000000000..9e795bb8ca --- /dev/null +++ b/openpype/modules/default_modules/kitsu/utils/openpype.py @@ -0,0 +1,47 @@ +import gazu + +from pymongo import DeleteOne, UpdateOne + +from avalon.api import AvalonMongoDB +from openpype.lib import create_project + + +def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: + """Sync project with database. + Create project if doesn't exist. + + :param project: Gazu project + :param dbcon: DB to create project in + :return: Update instance for the project + """ + project_name = project["name"] + project_doc = dbcon.find_one({"type": "project"}) + if not project_doc: + print(f"Creating project '{project_name}'") + project_doc = create_project(project_name, project_name, dbcon=dbcon) + + print(f"Synchronizing {project_name}...") + + # Project data and tasks + if not project["data"]: # Sentinel + project["data"] = {} + + return UpdateOne( + {"_id": project_doc["_id"]}, + { + "$set": { + "config.tasks": { + t["name"]: {"short_name": t.get("short_name", t["name"])} + for t in gazu.task.all_task_types_for_project(project) + }, + "data": project["data"].update( + { + "code": project["code"], + "fps": project["fps"], + "resolutionWidth": project["resolution"].split("x")[0], + "resolutionHeight": project["resolution"].split("x")[1], + } + ), + } + }, + ) From 0d38ccc5127bd00157aa9d19157f5d7c3ec3e20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 23 Feb 2022 15:14:38 +0100 Subject: [PATCH 020/194] Listen asset and DRY --- .../default_modules/kitsu/kitsu_module.py | 117 ++++-------------- .../default_modules/kitsu/listeners.py | 70 +++++++++-- .../default_modules/kitsu/utils/openpype.py | 114 +++++++++++++++++ 3 files changed, 198 insertions(+), 103 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 3fddc48ee9..0bdd4b12e6 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -22,10 +22,7 @@ from gazu.shot import ( new_shot, ) from gazu.task import ( - all_tasks_for_asset, - all_tasks_for_shot, all_task_types, - all_task_types_for_project, new_task, new_task_type, ) @@ -33,9 +30,12 @@ from pymongo import DeleteOne, UpdateOne from avalon.api import AvalonMongoDB from openpype.api import get_project_settings -from openpype.lib import create_project from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager -from openpype.modules.default_modules.kitsu.utils.openpype import sync_project +from openpype.modules.default_modules.kitsu.utils.openpype import ( + create_op_asset, + sync_project, + update_op_assets, +) from openpype_interfaces import IPluginPaths, ITrayAction from .listeners import add_listeners @@ -417,33 +417,28 @@ def sync_openpype(): # Query all assets of the local project project_col = dbcon.database[project_code] - asset_doc_ids = { + zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in project_col.find({"type": "asset"}) if asset_doc["data"].get("zou", {}).get("id") } - asset_doc_ids[project["id"]] = project_doc + zou_ids_and_asset_docs[project["id"]] = project_doc # Create to_insert = [] to_insert.extend( [ - { - "name": item["name"], - "type": "asset", - "schema": "openpype:asset-3.0", - "data": {"zou": item, "tasks": {}}, - } + create_op_asset(item) for item in all_entities - if item["id"] not in asset_doc_ids.keys() + if item["id"] not in zou_ids_and_asset_docs.keys() ] ) if to_insert: - # Insert in doc + # Insert doc in DB project_col.insert_many(to_insert) # Update existing docs - asset_doc_ids.update( + zou_ids_and_asset_docs.update( { asset_doc["data"]["zou"]["id"]: asset_doc for asset_doc in project_col.find({"type": "asset"}) @@ -452,15 +447,23 @@ def sync_openpype(): ) # Update - bulk_writes.extend(update_op_assets(all_entities, asset_doc_ids)) + bulk_writes.extend( + [ + UpdateOne({"_id": id}, update) + for id, update in update_op_assets(all_entities, zou_ids_and_asset_docs) + ] + ) # Delete - diff_assets = set(asset_doc_ids.keys()) - { + diff_assets = set(zou_ids_and_asset_docs.keys()) - { e["id"] for e in all_entities + [project] } if diff_assets: bulk_writes.extend( - [DeleteOne(asset_doc_ids[asset_id]) for asset_id in diff_assets] + [ + DeleteOne(zou_ids_and_asset_docs[asset_id]) + for asset_id in diff_assets + ] ) # Write into DB @@ -470,82 +473,6 @@ def sync_openpype(): dbcon.uninstall() -def update_op_assets( - entities_list: List[dict], asset_doc_ids: Dict[str, dict] -) -> List[UpdateOne]: - """Update OpenPype assets. - Set 'data' and 'parent' fields. - - :param entities_list: List of zou entities to update - :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] - :return: List of UpdateOne objects - """ - bulk_writes = [] - for item in entities_list: - # Update asset - item_doc = asset_doc_ids[item["id"]] - item_data = item_doc["data"].copy() - item_data["zou"] = item - - # Tasks - tasks_list = None - if item["type"] == "Asset": - tasks_list = all_tasks_for_asset(item) - elif item["type"] == "Shot": - tasks_list = all_tasks_for_shot(item) - # TODO frame in and out - if tasks_list: - item_data["tasks"] = { - t["task_type_name"]: {"type": t["task_type_name"]} for t in tasks_list - } - - # Visual parent for hierarchy - substitute_parent_item = ( - item_data["parent_substitutes"][0] - if item_data.get("parent_substitutes") - else None - ) - parent_zou_id = item["parent_id"] or item["source_id"] - if substitute_parent_item: - parent_zou_id = ( - substitute_parent_item["parent_id"] - or substitute_parent_item["source_id"] - ) - visual_parent_doc = asset_doc_ids[parent_zou_id] - item_data["visualParent"] = visual_parent_doc["_id"] - - # Add parents for hierarchy - item_data["parents"] = [] - while parent_zou_id is not None: - parent_doc = asset_doc_ids[parent_zou_id] - item_data["parents"].insert(0, parent_doc["name"]) - - parent_zou_id = next( - i for i in entities_list if i["id"] == parent_doc["data"]["zou"]["id"] - )["parent_id"] - - # Update 'data' different in zou DB - updated_data = { - k: item_data[k] - for k in item_data.keys() - if item_doc["data"].get(k) != item_data[k] - } - if updated_data or not item_doc.get("parent"): - bulk_writes.append( - UpdateOne( - {"_id": item_doc["_id"]}, - { - "$set": { - "data": item_data, - "parent": asset_doc_ids[item["project_id"]]["_id"], - } - }, - ) - ) - - return bulk_writes - - @cli_main.command() def listen(): """Show ExampleAddon dialog. diff --git a/openpype/modules/default_modules/kitsu/listeners.py b/openpype/modules/default_modules/kitsu/listeners.py index 5303933bce..fd4cf6dd3d 100644 --- a/openpype/modules/default_modules/kitsu/listeners.py +++ b/openpype/modules/default_modules/kitsu/listeners.py @@ -5,7 +5,12 @@ import os from pymongo import DeleteOne, UpdateOne from avalon.api import AvalonMongoDB -from openpype.modules.default_modules.kitsu.utils.openpype import sync_project +from openpype.modules.default_modules.kitsu.utils.openpype import ( + create_op_asset, + set_op_project, + sync_project, + update_op_assets, +) def add_listeners(): @@ -15,19 +20,22 @@ def add_listeners(): # Authenticate gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) gazu.set_event_host(os.environ["KITSU_SERVER"].replace("api", "socket.io")) + event_client = gazu.events.init() # Connect to DB dbcon = AvalonMongoDB() dbcon.install() + # == Project == + def new_project(data): - """Create new project into DB.""" + """Create new project into OP DB.""" # Use update process to avoid duplicating code update_project(data) def update_project(data): - """Update project into DB.""" + """Update project into OP DB.""" # Get project entity project = gazu.project.get_project(data["project_id"]) project_name = project["name"] @@ -41,6 +49,7 @@ def add_listeners(): project_col.bulk_write([update_project]) def delete_project(data): + """Delete project.""" # Get project entity print(data) # TODO check bugfix project = gazu.project.get_project(data["project_id"]) @@ -49,13 +58,58 @@ def add_listeners(): project_col = dbcon.database[project["name"]] project_col.drop() - def new_asset(data): - print("Asset created %s" % data) - - event_client = gazu.events.init() gazu.events.add_listener(event_client, "project:new", new_project) gazu.events.add_listener(event_client, "project:update", update_project) gazu.events.add_listener(event_client, "project:delete", delete_project) + + # == Asset == + + def new_asset(data): + """Create new asset into OP DB.""" + # Get project entity + project_col = set_op_project(dbcon, data["project_id"]) + + # Get gazu entity + asset = gazu.asset.get_asset(data["asset_id"]) + + # Insert doc in DB + project_col.insert_one(create_op_asset(asset)) + + # Update + update_asset(data) + + def update_asset(data): + """Update asset into OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + project_doc = dbcon.find_one({"type": "project"}) + + # Get gazu entity + 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 + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou", {}).get("id") + } + zou_ids_and_asset_docs[asset["project_id"]] = project_doc + + # Update + asset_doc_id, asset_update = update_op_assets([asset], zou_ids_and_asset_docs)[ + 0 + ] + project_col.update_one({"_id": asset_doc_id}, asset_update) + + def delete_asset(data): + """Delete asset of OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + + # Delete + project_col.delete_one({"type": "asset", "data.zou.id": data["asset_id"]}) + gazu.events.add_listener(event_client, "asset:new", new_asset) + gazu.events.add_listener(event_client, "asset:update", update_asset) + gazu.events.add_listener(event_client, "asset:delete", delete_asset) + gazu.events.run_client(event_client) - print("ll") diff --git a/openpype/modules/default_modules/kitsu/utils/openpype.py b/openpype/modules/default_modules/kitsu/utils/openpype.py index 9e795bb8ca..8ef987898d 100644 --- a/openpype/modules/default_modules/kitsu/utils/openpype.py +++ b/openpype/modules/default_modules/kitsu/utils/openpype.py @@ -1,11 +1,125 @@ +from typing import Dict, List import gazu from pymongo import DeleteOne, UpdateOne +from pymongo.collection import Collection + +from gazu.task import ( + all_tasks_for_asset, + all_tasks_for_shot, +) from avalon.api import AvalonMongoDB from openpype.lib import create_project +def create_op_asset(gazu_entity: dict) -> dict: + """Create OP asset dict from gazu entity. + + :param gazu_entity: + """ + return { + "name": gazu_entity["name"], + "type": "asset", + "schema": "openpype:asset-3.0", + "data": {"zou": gazu_entity, "tasks": {}}, + } + + +def set_op_project(dbcon, project_id) -> Collection: + """Set project context. + + :param dbcon: Connection to DB. + :param project_id: Project zou ID + """ + project = gazu.project.get_project(project_id) + project_name = project["name"] + dbcon.Session["AVALON_PROJECT"] = project_name + + return dbcon.database[project_name] + + +def update_op_assets( + entities_list: List[dict], asset_doc_ids: Dict[str, dict] +) -> List[Dict[str, dict]]: + """Update OpenPype assets. + Set 'data' and 'parent' fields. + + :param entities_list: List of zou entities to update + :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] + :return: List of (doc_id, update_dict) tuples + """ + assets_with_update = [] + for item in entities_list: + # Update asset + item_doc = asset_doc_ids[item["id"]] + item_data = item_doc["data"].copy() + item_data["zou"] = item + + # Tasks + tasks_list = [] + if item["type"] == "Asset": + tasks_list = all_tasks_for_asset(item) + elif item["type"] == "Shot": + tasks_list = all_tasks_for_shot(item) + # TODO frame in and out + item_data["tasks"] = { + t["task_type_name"]: {"type": t["task_type_name"]} for t in tasks_list + } + + # Get zou parent id for correct hierarchy + # Use parent substitutes if existing + substitute_parent_item = ( + item_data["parent_substitutes"][0] + if item_data.get("parent_substitutes") + else None + ) + if substitute_parent_item: + parent_zou_id = substitute_parent_item["id"] + else: + parent_zou_id = ( + item.get("parent_id") or item.get("episode_id") or item.get("source_id") + ) # TODO check consistency + + # Visual parent for hierarchy + visual_parent_doc_id = ( + asset_doc_ids[parent_zou_id]["_id"] if parent_zou_id else None + ) + item_data["visualParent"] = visual_parent_doc_id + + # Add parents for hierarchy + item_data["parents"] = [] + while parent_zou_id is not None: + parent_doc = asset_doc_ids[parent_zou_id] + item_data["parents"].insert(0, parent_doc["name"]) + + # Get parent entity + parent_entity = parent_doc["data"]["zou"] + parent_zou_id = parent_entity["parent_id"] + + # Update 'data' different in zou DB + updated_data = { + k: item_data[k] + for k in item_data.keys() + if item_doc["data"].get(k) != item_data[k] + } + if updated_data or not item_doc.get("parent"): + assets_with_update.append( + ( + item_doc["_id"], + { + "$set": { + "name": item["name"], + "data": item_data, + "parent": asset_doc_ids[item["project_id"]]["_id"], + } + }, + ) + ) + + return assets_with_update + + def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: """Sync project with database. Create project if doesn't exist. From fe5486886712a77cd40f37cac6ec41d329c17748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 23 Feb 2022 17:04:21 +0100 Subject: [PATCH 021/194] Listen Episode, Sequence and Shot --- .../default_modules/kitsu/listeners.py | 147 ++++++++++++++++++ .../default_modules/kitsu/utils/openpype.py | 2 +- 2 files changed, 148 insertions(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/kitsu/listeners.py b/openpype/modules/default_modules/kitsu/listeners.py index fd4cf6dd3d..bb217452df 100644 --- a/openpype/modules/default_modules/kitsu/listeners.py +++ b/openpype/modules/default_modules/kitsu/listeners.py @@ -112,4 +112,151 @@ def add_listeners(): gazu.events.add_listener(event_client, "asset:update", update_asset) gazu.events.add_listener(event_client, "asset:delete", delete_asset) + # == Episode == + def new_episode(data): + """Create new episode into OP DB.""" + # Get project entity + project_col = set_op_project(dbcon, data["project_id"]) + + # Get gazu entity + episode = gazu.shot.get_episode(data["episode_id"]) + + # Insert doc in DB + project_col.insert_one(create_op_asset(episode)) + + # Update + update_episode(data) + + def update_episode(data): + """Update episode into OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + project_doc = dbcon.find_one({"type": "project"}) + + # Get gazu entity + 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 + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou", {}).get("id") + } + zou_ids_and_asset_docs[episode["project_id"]] = project_doc + + # Update + asset_doc_id, asset_update = update_op_assets( + [episode], zou_ids_and_asset_docs + )[0] + project_col.update_one({"_id": asset_doc_id}, asset_update) + + def delete_episode(data): + """Delete shot of OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + print("delete episode") # TODO check bugfix + + # Delete + project_col.delete_one({"type": "asset", "data.zou.id": data["episode_id"]}) + + gazu.events.add_listener(event_client, "episode:new", new_episode) + gazu.events.add_listener(event_client, "episode:update", update_episode) + gazu.events.add_listener(event_client, "episode:delete", delete_episode) + + # == Sequence == + def new_sequence(data): + """Create new sequnce into OP DB.""" + # Get project entity + project_col = set_op_project(dbcon, data["project_id"]) + + # Get gazu entity + sequence = gazu.shot.get_sequence(data["sequence_id"]) + + # Insert doc in DB + project_col.insert_one(create_op_asset(sequence)) + + # Update + update_sequence(data) + + def update_sequence(data): + """Update sequence into OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + project_doc = dbcon.find_one({"type": "project"}) + + # Get gazu entity + 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 + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou", {}).get("id") + } + zou_ids_and_asset_docs[sequence["project_id"]] = project_doc + + # Update + asset_doc_id, asset_update = update_op_assets( + [sequence], zou_ids_and_asset_docs + )[0] + project_col.update_one({"_id": asset_doc_id}, asset_update) + + def delete_sequence(data): + """Delete sequence of OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + print("delete sequence") # TODO check bugfix + + # Delete + project_col.delete_one({"type": "asset", "data.zou.id": data["sequence_id"]}) + + gazu.events.add_listener(event_client, "sequence:new", new_sequence) + gazu.events.add_listener(event_client, "sequence:update", update_sequence) + gazu.events.add_listener(event_client, "sequence:delete", delete_sequence) + + # == Shot == + def new_shot(data): + """Create new shot into OP DB.""" + # Get project entity + project_col = set_op_project(dbcon, data["project_id"]) + + # Get gazu entity + shot = gazu.shot.get_shot(data["shot_id"]) + + # Insert doc in DB + project_col.insert_one(create_op_asset(shot)) + + # Update + update_shot(data) + + def update_shot(data): + """Update shot into OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + project_doc = dbcon.find_one({"type": "project"}) + + # Get gazu entity + 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 + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou", {}).get("id") + } + zou_ids_and_asset_docs[shot["project_id"]] = project_doc + + # Update + asset_doc_id, asset_update = update_op_assets([shot], zou_ids_and_asset_docs)[0] + project_col.update_one({"_id": asset_doc_id}, asset_update) + + def delete_shot(data): + """Delete shot of OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + + # Delete + project_col.delete_one({"type": "asset", "data.zou.id": data["shot_id"]}) + + gazu.events.add_listener(event_client, "shot:new", new_shot) + gazu.events.add_listener(event_client, "shot:update", update_shot) + gazu.events.add_listener(event_client, "shot:delete", delete_shot) + gazu.events.run_client(event_client) diff --git a/openpype/modules/default_modules/kitsu/utils/openpype.py b/openpype/modules/default_modules/kitsu/utils/openpype.py index 8ef987898d..809748fa78 100644 --- a/openpype/modules/default_modules/kitsu/utils/openpype.py +++ b/openpype/modules/default_modules/kitsu/utils/openpype.py @@ -75,7 +75,7 @@ def update_op_assets( else None ) if substitute_parent_item: - parent_zou_id = substitute_parent_item["id"] + parent_zou_id = substitute_parent_item["parent_id"] else: parent_zou_id = ( item.get("parent_id") or item.get("episode_id") or item.get("source_id") From de153a0450d9cbdcf9229775fa8fa9afa94eef79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 24 Feb 2022 15:53:03 +0100 Subject: [PATCH 022/194] Listen tasks, clean code --- .../default_modules/kitsu/kitsu_module.py | 1 - .../default_modules/kitsu/listeners.py | 55 ++++++++++++++++++- .../default_modules/kitsu/utils/openpype.py | 2 +- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 0bdd4b12e6..10cbe76db2 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -9,7 +9,6 @@ in global space here until are required or used. import click import os import re -from typing import Dict, List import gazu from gazu.asset import all_assets_for_project, all_asset_types, new_asset diff --git a/openpype/modules/default_modules/kitsu/listeners.py b/openpype/modules/default_modules/kitsu/listeners.py index bb217452df..16c4e0e69e 100644 --- a/openpype/modules/default_modules/kitsu/listeners.py +++ b/openpype/modules/default_modules/kitsu/listeners.py @@ -1,9 +1,6 @@ -from turtle import update import gazu import os -from pymongo import DeleteOne, UpdateOne - from avalon.api import AvalonMongoDB from openpype.modules.default_modules.kitsu.utils.openpype import ( create_op_asset, @@ -259,4 +256,56 @@ def add_listeners(): gazu.events.add_listener(event_client, "shot:update", update_shot) gazu.events.add_listener(event_client, "shot:delete", delete_shot) + # == Task == + def new_task(data): + """Create new task into OP DB.""" + print("new", data) + # Get project entity + project_col = set_op_project(dbcon, data["project_id"]) + + # Get gazu entity + task = gazu.task.get_task(data["task_id"]) + + # Find asset doc + asset_doc = project_col.find_one( + {"type": "asset", "data.zou.id": task["entity"]["id"]} + ) + + # 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} + project_col.update_one( + {"_id": asset_doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} + ) + + def update_task(data): + """Update task into OP DB.""" + # TODO is it necessary? + pass + + def delete_task(data): + """Delete task of OP DB.""" + project_col = set_op_project(dbcon, data["project_id"]) + + # Find asset doc + asset_docs = [doc for doc in project_col.find({"type": "asset"})] + 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"]: + # Pop task + asset_tasks = doc["data"].get("tasks") + asset_tasks.pop(name) + + # Delete task in DB + project_col.update_one( + {"_id": doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} + ) + return + + gazu.events.add_listener(event_client, "task:new", new_task) + gazu.events.add_listener(event_client, "task:update", update_task) + gazu.events.add_listener(event_client, "task:delete", delete_task) + gazu.events.run_client(event_client) diff --git a/openpype/modules/default_modules/kitsu/utils/openpype.py b/openpype/modules/default_modules/kitsu/utils/openpype.py index 809748fa78..edcf233ea8 100644 --- a/openpype/modules/default_modules/kitsu/utils/openpype.py +++ b/openpype/modules/default_modules/kitsu/utils/openpype.py @@ -1,7 +1,7 @@ from typing import Dict, List import gazu -from pymongo import DeleteOne, UpdateOne +from pymongo import UpdateOne from pymongo.collection import Collection from gazu.task import ( From 6cf38e1a35ba73a5df64969befb71728ffec4813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 25 Feb 2022 16:23:14 +0100 Subject: [PATCH 023/194] Publish comment to kitsu --- .../default_modules/kitsu/kitsu_module.py | 2 +- .../kitsu/plugins/publish/example_plugin.py | 40 +++++++++++++++++++ .../default_modules/kitsu/utils/openpype.py | 24 +++++------ 3 files changed, 53 insertions(+), 13 deletions(-) diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index 10cbe76db2..f62a86f04d 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -408,7 +408,7 @@ def sync_openpype(): all_entities = [ e for e in all_assets + all_episodes + all_seqs + all_shots - if not e["data"].get("is_substitute") + if e["data"] and not e["data"].get("is_substitute") ] # Sync project. Create if doesn't exist diff --git a/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py b/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py index 61602f4e78..614d9ecc38 100644 --- a/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py +++ b/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py @@ -1,9 +1,49 @@ +import os + +import gazu + import pyblish.api +import debugpy + class CollectExampleAddon(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.4 label = "Collect Kitsu" def process(self, context): + debugpy.breakpoint() self.log.info("I'm in Kitsu's plugin!") + + +class IntegrateRig(pyblish.api.InstancePlugin): + """Copy files to an appropriate location where others may reach it""" + + order = pyblish.api.IntegratorOrder + families = ["model"] + + def process(self, instance): + print(instance.data["version"]) + + # Connect to server + gazu.client.set_host(os.environ["KITSU_SERVER"]) + + # Authenticate + gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) + + asset_data = instance.data["assetEntity"]["data"] + + # TODO Set local settings for login and password + + # Get task + task_type = gazu.task.get_task_type_by_name(instance.data["task"]) + entity_task = gazu.task.get_task_by_entity(asset_data["zou"], task_type) + + # Comment entity + gazu.task.add_comment( + entity_task, + entity_task["task_status_id"], + comment=f"Version {instance.data['version']} has been published!", + ) + + self.log.info("Copied successfully!") diff --git a/openpype/modules/default_modules/kitsu/utils/openpype.py b/openpype/modules/default_modules/kitsu/utils/openpype.py index edcf233ea8..518872a71c 100644 --- a/openpype/modules/default_modules/kitsu/utils/openpype.py +++ b/openpype/modules/default_modules/kitsu/utils/openpype.py @@ -134,11 +134,18 @@ def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: print(f"Creating project '{project_name}'") project_doc = create_project(project_name, project_name, dbcon=dbcon) - print(f"Synchronizing {project_name}...") - # Project data and tasks - if not project["data"]: # Sentinel - project["data"] = {} + project_data = project["data"] or {} + + # Update data + project_data.update( + { + "code": project["code"], + "fps": project["fps"], + "resolutionWidth": project["resolution"].split("x")[0], + "resolutionHeight": project["resolution"].split("x")[1], + } + ) return UpdateOne( {"_id": project_doc["_id"]}, @@ -148,14 +155,7 @@ def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) }, - "data": project["data"].update( - { - "code": project["code"], - "fps": project["fps"], - "resolutionWidth": project["resolution"].split("x")[0], - "resolutionHeight": project["resolution"].split("x")[1], - } - ), + "data": project_data, } }, ) From cb01f8338c3b651897d626479a1a1bd306a7e05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 1 Mar 2022 17:56:08 +0100 Subject: [PATCH 024/194] Sign in dialog, credentials as local setting and cleaning --- .../default_modules/kitsu/kitsu_module.py | 72 +++---- .../default_modules/kitsu/kitsu_widgets.py | 193 ++++++++++++++++++ .../{example_plugin.py => kitsu_plugin.py} | 8 +- .../schemas/project_schemas/main.json | 30 --- .../schemas/project_schemas/the_template.json | 30 --- .../kitsu/{ => utils}/listeners.py | 2 +- .../modules/default_modules/kitsu/widgets.py | 31 --- .../defaults/system_settings/modules.json | 4 +- .../module_settings/schema_kitsu.json | 10 - pyproject.toml | 3 +- 10 files changed, 224 insertions(+), 159 deletions(-) create mode 100644 openpype/modules/default_modules/kitsu/kitsu_widgets.py rename openpype/modules/default_modules/kitsu/plugins/publish/{example_plugin.py => kitsu_plugin.py} (85%) delete mode 100644 openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json delete mode 100644 openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json rename openpype/modules/default_modules/kitsu/{ => utils}/listeners.py (99%) delete mode 100644 openpype/modules/default_modules/kitsu/widgets.py diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/default_modules/kitsu/kitsu_module.py index f62a86f04d..51ab8aaa42 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/default_modules/kitsu/kitsu_module.py @@ -1,11 +1,5 @@ -"""Addon definition is located here. +"""Kitsu module.""" -Import of python packages that may not be available should not be imported -in global space here until are required or used. -- Qt related imports -- imports of Python 3 packages - - we still support Python 2 hosts where addon definition should available -""" import click import os import re @@ -35,20 +29,12 @@ from openpype.modules.default_modules.kitsu.utils.openpype import ( sync_project, update_op_assets, ) +from openpype.settings.lib import get_local_settings from openpype_interfaces import IPluginPaths, ITrayAction -from .listeners import add_listeners +from .utils.listeners import start_listeners -# Settings definition of this addon using `JsonFilesSettingsDef` -# - JsonFilesSettingsDef is prepared settings definition using json files -# to define settings and store default values class AddonSettingsDef(JsonFilesSettingsDef): - # This will add prefixes to every schema and template from `schemas` - # subfolder. - # - it is not required to fill the prefix but it is highly - # recommended as schemas and templates may have name clashes across - # multiple addons - # - it is also recommended that prefix has addon name in it schema_prefix = "kitsu" def get_settings_root_path(self): @@ -61,20 +47,15 @@ class AddonSettingsDef(JsonFilesSettingsDef): class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): - """This Addon has defined it's settings and interface. - - This example has system settings with an enabled option. And use - few other interfaces: - - `IPluginPaths` to define custom plugin paths - - `ITrayAction` to be shown in tray tool - """ + """Kitsu module class.""" label = "Kitsu" name = "kitsu" def initialize(self, settings): - """Initialization of addon.""" + """Initialization of module.""" module_settings = settings[self.name] + local_kitsu_settings = get_local_settings().get("kitsu", {}) # Enabled by settings self.enabled = module_settings.get("enabled", False) @@ -93,8 +74,8 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): self.server_url = kitsu_url # Set credentials - self.script_login = module_settings["script_login"] - self.script_pwd = module_settings["script_pwd"] + self.kitsu_login = local_kitsu_settings["login"] + self.kitsu_password = local_kitsu_settings["password"] # Prepare variables that can be used or set afterwards self._connected_modules = None @@ -113,8 +94,8 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Kitsu's global environments.""" return { "KITSU_SERVER": self.server_url, - "KITSU_LOGIN": self.script_login, - "KITSU_PWD": self.script_pwd, + "KITSU_LOGIN": self.kitsu_login, + "KITSU_PWD": self.kitsu_password, } def _create_dialog(self): @@ -122,9 +103,9 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): if self._dialog is not None: return - from .widgets import MyExampleDialog + from .kitsu_widgets import PasswordDialog - self._dialog = MyExampleDialog() + self._dialog = PasswordDialog() def show_dialog(self): """Show dialog with connected modules. @@ -376,7 +357,10 @@ def sync_zou(): @cli_main.command() -def sync_openpype(): +@click.option( + "-l", "--listen", is_flag=True, help="Listen Kitsu server after synchronization." +) +def sync_openpype(listen: bool): """Synchronize openpype database from Zou sever database.""" # Connect to server @@ -471,27 +455,23 @@ def sync_openpype(): dbcon.uninstall() + # Run listening + if listen: + start_listeners() + @cli_main.command() def listen(): - """Show ExampleAddon dialog. - - We don't have access to addon directly through cli so we have to create - it again. - """ - add_listeners() + """Listen to Kitsu server.""" + start_listeners() @cli_main.command() -def show_dialog(): - """Show ExampleAddon dialog. - - We don't have access to addon directly through cli so we have to create - it again. - """ +def sign_in(): + """Show credentials dialog.""" from openpype.tools.utils.lib import qt_app_context manager = ModulesManager() - example_addon = manager.modules_by_name[KitsuModule.name] + kitsu_addon = manager.modules_by_name[KitsuModule.name] with qt_app_context(): - example_addon.show_dialog() + kitsu_addon.show_dialog() diff --git a/openpype/modules/default_modules/kitsu/kitsu_widgets.py b/openpype/modules/default_modules/kitsu/kitsu_widgets.py new file mode 100644 index 0000000000..6bd436f460 --- /dev/null +++ b/openpype/modules/default_modules/kitsu/kitsu_widgets.py @@ -0,0 +1,193 @@ +import os + +import gazu +from Qt import QtWidgets, QtCore, QtGui + +from openpype import style +from openpype.resources import get_resource +from openpype.settings.lib import ( + get_local_settings, + get_system_settings, + save_local_settings, +) + +from openpype.widgets.password_dialog import PressHoverButton + + +class PasswordDialog(QtWidgets.QDialog): + """Stupidly simple dialog to compare password from general settings.""" + + finished = QtCore.Signal(bool) + + def __init__(self, parent=None): + super(PasswordDialog, self).__init__(parent) + + self.setWindowTitle("Kitsu Credentials") + self.resize(300, 120) + + system_settings = get_system_settings() + kitsu_settings = get_local_settings().get("kitsu", {}) + remembered = kitsu_settings.get("remember") + + self._final_result = None + self._connectable = bool( + system_settings["modules"].get("kitsu", {}).get("server") + ) + + # Server label + server_label = QtWidgets.QLabel( + f"Server: {system_settings['modules']['kitsu']['server'] if self._connectable else 'no server url set in Studio Settings...'}", + self, + ) + + # Login input + login_widget = QtWidgets.QWidget(self) + + login_label = QtWidgets.QLabel("Login:", login_widget) + + login_input = QtWidgets.QLineEdit( + login_widget, text=kitsu_settings.get("login") if remembered else None + ) + login_input.setPlaceholderText("Your Kitsu account login...") + + login_layout = QtWidgets.QHBoxLayout(login_widget) + login_layout.setContentsMargins(0, 0, 0, 0) + login_layout.addWidget(login_label) + login_layout.addWidget(login_input) + + # Password input + password_widget = QtWidgets.QWidget(self) + + password_label = QtWidgets.QLabel("Password:", password_widget) + + password_input = QtWidgets.QLineEdit( + password_widget, text=kitsu_settings.get("password") if remembered else None + ) + password_input.setPlaceholderText("Your password...") + password_input.setEchoMode(QtWidgets.QLineEdit.Password) + + show_password_icon_path = get_resource("icons", "eye.png") + show_password_icon = QtGui.QIcon(show_password_icon_path) + show_password_btn = PressHoverButton(password_widget) + show_password_btn.setObjectName("PasswordBtn") + show_password_btn.setIcon(show_password_icon) + show_password_btn.setFocusPolicy(QtCore.Qt.ClickFocus) + + password_layout = QtWidgets.QHBoxLayout(password_widget) + password_layout.setContentsMargins(0, 0, 0, 0) + password_layout.addWidget(password_label) + password_layout.addWidget(password_input) + password_layout.addWidget(show_password_btn) + + # Message label + message_label = QtWidgets.QLabel("", self) + + # Buttons + buttons_widget = QtWidgets.QWidget(self) + + remember_checkbox = QtWidgets.QCheckBox("Remember", buttons_widget) + remember_checkbox.setObjectName("RememberCheckbox") + remember_checkbox.setChecked(remembered if remembered is not None else True) + + ok_btn = QtWidgets.QPushButton("Ok", buttons_widget) + cancel_btn = QtWidgets.QPushButton("Cancel", buttons_widget) + + buttons_layout = QtWidgets.QHBoxLayout(buttons_widget) + buttons_layout.setContentsMargins(0, 0, 0, 0) + buttons_layout.addWidget(remember_checkbox) + buttons_layout.addStretch(1) + buttons_layout.addWidget(ok_btn) + buttons_layout.addWidget(cancel_btn) + + # Main layout + layout = QtWidgets.QVBoxLayout(self) + layout.addSpacing(5) + layout.addWidget(server_label, 0) + layout.addSpacing(5) + layout.addWidget(login_widget, 0) + layout.addWidget(password_widget, 0) + layout.addWidget(message_label, 0) + layout.addStretch(1) + layout.addWidget(buttons_widget, 0) + + ok_btn.clicked.connect(self._on_ok_click) + cancel_btn.clicked.connect(self._on_cancel_click) + show_password_btn.change_state.connect(self._on_show_password) + + self.login_input = login_input + self.password_input = password_input + self.remember_checkbox = remember_checkbox + self.message_label = message_label + + self.setStyleSheet(style.load_stylesheet()) + + def result(self): + return self._final_result + + def keyPressEvent(self, event): + if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter): + self._on_ok_click() + return event.accept() + super(PasswordDialog, self).keyPressEvent(event) + + def closeEvent(self, event): + super(PasswordDialog, self).closeEvent(event) + self.finished.emit(self.result()) + + def _on_ok_click(self): + # Check if is connectable + if not self._connectable: + self.message_label.setText("Please set server url in Studio Settings!") + return + + # Collect values + login_value = self.login_input.text() + pwd_value = self.password_input.text() + remember = self.remember_checkbox.isChecked() + + # Connect to server + gazu.client.set_host(os.environ["KITSU_SERVER"]) + + # Authenticate + gazu.log_in(login_value, pwd_value) + + # Set logging-in env vars + os.environ["KITSU_LOGIN"] = login_value + os.environ["KITSU_PWD"] = pwd_value + + # Get settings + local_settings = get_local_settings() + local_settings.setdefault("kitsu", {}) + + # Remember password cases + if remember: + # Set local settings + local_settings["kitsu"]["login"] = login_value + local_settings["kitsu"]["password"] = pwd_value + else: + # Clear local settings + local_settings["kitsu"]["login"] = None + local_settings["kitsu"]["password"] = None + + # Clear input fields + self.login_input.clear() + self.password_input.clear() + + # Keep 'remember' parameter + local_settings["kitsu"]["remember"] = remember + + # Save settings + save_local_settings(local_settings) + + self._final_result = True + self.close() + + def _on_show_password(self, show_password): + if show_password: + echo_mode = QtWidgets.QLineEdit.Normal + else: + echo_mode = QtWidgets.QLineEdit.Password + self.password_input.setEchoMode(echo_mode) + + def _on_cancel_click(self): + self.close() diff --git a/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py b/openpype/modules/default_modules/kitsu/plugins/publish/kitsu_plugin.py similarity index 85% rename from openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py rename to openpype/modules/default_modules/kitsu/plugins/publish/kitsu_plugin.py index 614d9ecc38..86ba40a5f4 100644 --- a/openpype/modules/default_modules/kitsu/plugins/publish/example_plugin.py +++ b/openpype/modules/default_modules/kitsu/plugins/publish/kitsu_plugin.py @@ -4,15 +4,12 @@ import gazu import pyblish.api -import debugpy - class CollectExampleAddon(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.4 label = "Collect Kitsu" def process(self, context): - debugpy.breakpoint() self.log.info("I'm in Kitsu's plugin!") @@ -23,7 +20,6 @@ class IntegrateRig(pyblish.api.InstancePlugin): families = ["model"] def process(self, instance): - print(instance.data["version"]) # Connect to server gazu.client.set_host(os.environ["KITSU_SERVER"]) @@ -33,8 +29,6 @@ class IntegrateRig(pyblish.api.InstancePlugin): asset_data = instance.data["assetEntity"]["data"] - # TODO Set local settings for login and password - # Get task task_type = gazu.task.get_task_type_by_name(instance.data["task"]) entity_task = gazu.task.get_task_by_entity(asset_data["zou"], task_type) @@ -46,4 +40,4 @@ class IntegrateRig(pyblish.api.InstancePlugin): comment=f"Version {instance.data['version']} has been published!", ) - self.log.info("Copied successfully!") + self.log.info("Version published to Kitsu successfully!") diff --git a/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json b/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json deleted file mode 100644 index 82e58ce9ab..0000000000 --- a/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/main.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "type": "dict", - "key": "kitsu", - "label": " Kitsu", - "collapsible": true, - "children": [ - { - "type": "number", - "key": "number", - "label": "This is your lucky number:", - "minimum": 7, - "maximum": 7, - "decimals": 0 - }, - { - "type": "template", - "name": "kitsu/the_template", - "template_data": [ - { - "name": "color_1", - "label": "Color 1" - }, - { - "name": "color_2", - "label": "Color 2" - } - ] - } - ] -} diff --git a/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json b/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json deleted file mode 100644 index af8fd9dae4..0000000000 --- a/openpype/modules/default_modules/kitsu/settings/schemas/project_schemas/the_template.json +++ /dev/null @@ -1,30 +0,0 @@ -[ - { - "type": "list-strict", - "key": "{name}", - "label": "{label}", - "object_types": [ - { - "label": "Red", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Green", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - }, - { - "label": "Blue", - "type": "number", - "minimum": 0, - "maximum": 1, - "decimal": 3 - } - ] - } -] diff --git a/openpype/modules/default_modules/kitsu/listeners.py b/openpype/modules/default_modules/kitsu/utils/listeners.py similarity index 99% rename from openpype/modules/default_modules/kitsu/listeners.py rename to openpype/modules/default_modules/kitsu/utils/listeners.py index 16c4e0e69e..c99870fa36 100644 --- a/openpype/modules/default_modules/kitsu/listeners.py +++ b/openpype/modules/default_modules/kitsu/utils/listeners.py @@ -2,7 +2,7 @@ import gazu import os from avalon.api import AvalonMongoDB -from openpype.modules.default_modules.kitsu.utils.openpype import ( +from .openpype import ( create_op_asset, set_op_project, sync_project, diff --git a/openpype/modules/default_modules/kitsu/widgets.py b/openpype/modules/default_modules/kitsu/widgets.py deleted file mode 100644 index de232113fe..0000000000 --- a/openpype/modules/default_modules/kitsu/widgets.py +++ /dev/null @@ -1,31 +0,0 @@ -from Qt import QtWidgets - -from openpype.style import load_stylesheet - - -class MyExampleDialog(QtWidgets.QDialog): - def __init__(self, parent=None): - super(MyExampleDialog, self).__init__(parent) - - self.setWindowTitle("Connected modules") - - msg = "This is example dialog of Kitsu." - label_widget = QtWidgets.QLabel(msg, self) - - ok_btn = QtWidgets.QPushButton("OK", self) - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.addStretch(1) - btns_layout.addWidget(ok_btn) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(label_widget) - layout.addLayout(btns_layout) - - ok_btn.clicked.connect(self._on_ok_clicked) - - self._label_widget = label_widget - - self.setStyleSheet(load_stylesheet()) - - def _on_ok_clicked(self): - self.done(1) diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index ddb2edc360..537e287366 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -139,9 +139,7 @@ }, "kitsu": { "enabled": false, - "server": "", - "script_login": "", - "script_pwd": "" + "server": "" }, "timers_manager": { "enabled": true, diff --git a/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json b/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json index ae2b52df0d..15a2ccc58d 100644 --- a/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json +++ b/openpype/settings/entities/schemas/system_schema/module_settings/schema_kitsu.json @@ -16,16 +16,6 @@ "key": "server", "label": "Server" }, - { - "type": "text", - "key": "script_login", - "label": "Script Login" - }, - { - "type": "text", - "key": "script_pwd", - "label": "Script Password" - }, { "type": "splitter" } diff --git a/pyproject.toml b/pyproject.toml index f32e385e80..93caa5ca70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ clique = "1.6.*" Click = "^7" dnspython = "^2.1.0" ftrack-python-api = "2.0.*" +gazu = "^0.8" google-api-python-client = "^1.12.8" # sync server google support (should be separate?) jsonschema = "^2.6.0" keyring = "^22.0.1" @@ -64,7 +65,7 @@ jinxed = [ python3-xlib = { version="*", markers = "sys_platform == 'linux'"} enlighten = "^1.9.0" slack-sdk = "^3.6.0" -requests = "2.25.1" +requests = "^2.25.1" pysftp = "^0.2.9" dropbox = "^11.20.0" From 6b3987708711e72d984b39096efec9a0421ffd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 1 Mar 2022 18:11:42 +0100 Subject: [PATCH 025/194] Moved to modules --- openpype/modules/{default_modules => }/kitsu/__init__.py | 0 openpype/modules/{default_modules => }/kitsu/kitsu_module.py | 2 +- openpype/modules/{default_modules => }/kitsu/kitsu_widgets.py | 0 .../kitsu/plugins/publish/kitsu_plugin.py | 0 openpype/modules/{default_modules => }/kitsu/utils/__init__.py | 0 .../modules/{default_modules => }/kitsu/utils/listeners.py | 3 ++- openpype/modules/{default_modules => }/kitsu/utils/openpype.py | 0 7 files changed, 3 insertions(+), 2 deletions(-) rename openpype/modules/{default_modules => }/kitsu/__init__.py (100%) rename openpype/modules/{default_modules => }/kitsu/kitsu_module.py (99%) rename openpype/modules/{default_modules => }/kitsu/kitsu_widgets.py (100%) rename openpype/modules/{default_modules => }/kitsu/plugins/publish/kitsu_plugin.py (100%) rename openpype/modules/{default_modules => }/kitsu/utils/__init__.py (100%) rename openpype/modules/{default_modules => }/kitsu/utils/listeners.py (99%) rename openpype/modules/{default_modules => }/kitsu/utils/openpype.py (100%) diff --git a/openpype/modules/default_modules/kitsu/__init__.py b/openpype/modules/kitsu/__init__.py similarity index 100% rename from openpype/modules/default_modules/kitsu/__init__.py rename to openpype/modules/kitsu/__init__.py diff --git a/openpype/modules/default_modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py similarity index 99% rename from openpype/modules/default_modules/kitsu/kitsu_module.py rename to openpype/modules/kitsu/kitsu_module.py index 51ab8aaa42..a17b509047 100644 --- a/openpype/modules/default_modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -24,7 +24,7 @@ from pymongo import DeleteOne, UpdateOne from avalon.api import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager -from openpype.modules.default_modules.kitsu.utils.openpype import ( +from openpype.modules.kitsu.utils.openpype import ( create_op_asset, sync_project, update_op_assets, diff --git a/openpype/modules/default_modules/kitsu/kitsu_widgets.py b/openpype/modules/kitsu/kitsu_widgets.py similarity index 100% rename from openpype/modules/default_modules/kitsu/kitsu_widgets.py rename to openpype/modules/kitsu/kitsu_widgets.py diff --git a/openpype/modules/default_modules/kitsu/plugins/publish/kitsu_plugin.py b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py similarity index 100% rename from openpype/modules/default_modules/kitsu/plugins/publish/kitsu_plugin.py rename to openpype/modules/kitsu/plugins/publish/kitsu_plugin.py diff --git a/openpype/modules/default_modules/kitsu/utils/__init__.py b/openpype/modules/kitsu/utils/__init__.py similarity index 100% rename from openpype/modules/default_modules/kitsu/utils/__init__.py rename to openpype/modules/kitsu/utils/__init__.py diff --git a/openpype/modules/default_modules/kitsu/utils/listeners.py b/openpype/modules/kitsu/utils/listeners.py similarity index 99% rename from openpype/modules/default_modules/kitsu/utils/listeners.py rename to openpype/modules/kitsu/utils/listeners.py index c99870fa36..961aa1691b 100644 --- a/openpype/modules/default_modules/kitsu/utils/listeners.py +++ b/openpype/modules/kitsu/utils/listeners.py @@ -10,7 +10,8 @@ from .openpype import ( ) -def add_listeners(): +def start_listeners(): + """Start listeners to keep OpenPype up-to-date with Kitsu.""" # Connect to server gazu.client.set_host(os.environ["KITSU_SERVER"]) diff --git a/openpype/modules/default_modules/kitsu/utils/openpype.py b/openpype/modules/kitsu/utils/openpype.py similarity index 100% rename from openpype/modules/default_modules/kitsu/utils/openpype.py rename to openpype/modules/kitsu/utils/openpype.py From a33b3d3b5cdfa37204da8570d175997be3544001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 09:30:33 +0100 Subject: [PATCH 026/194] Remove AddonSettingsDef --- openpype/modules/kitsu/__init__.py | 4 ++-- openpype/modules/kitsu/kitsu_module.py | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/openpype/modules/kitsu/__init__.py b/openpype/modules/kitsu/__init__.py index dc7c2dad50..6cb62bbb15 100644 --- a/openpype/modules/kitsu/__init__.py +++ b/openpype/modules/kitsu/__init__.py @@ -4,6 +4,6 @@ If addon class or settings definition won't be here their definition won't be found by OpenPype discovery. """ -from .kitsu_module import AddonSettingsDef, KitsuModule +from .kitsu_module import KitsuModule -__all__ = ("AddonSettingsDef", "KitsuModule") +__all__ = "KitsuModule" diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index a17b509047..9efcac9714 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -34,18 +34,6 @@ from openpype_interfaces import IPluginPaths, ITrayAction from .utils.listeners import start_listeners -class AddonSettingsDef(JsonFilesSettingsDef): - schema_prefix = "kitsu" - - def get_settings_root_path(self): - """Implemented abstract class of JsonFilesSettingsDef. - - Return directory path where json files defying addon settings are - located. - """ - return os.path.join(os.path.dirname(os.path.abspath(__file__)), "settings") - - class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Kitsu module class.""" From 4907ce1362602efef2692253d9b93fbc79d2e522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 09:31:02 +0100 Subject: [PATCH 027/194] Moved up module 'base' changes --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 0dd512ee8b..d77189be6c 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -860,6 +860,7 @@ class TrayModulesManager(ModulesManager): modules_menu_order = ( "user", "ftrack", + "kitsu", "muster", "launcher_tool", "avalon", From a15c4d2895a5eaf433ec9d3912200a271cac16a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 09:48:39 +0100 Subject: [PATCH 028/194] import gazu only at start --- openpype/modules/kitsu/kitsu_module.py | 63 ++++++++++------------- openpype/modules/kitsu/utils/listeners.py | 3 +- openpype/modules/kitsu/utils/openpype.py | 13 ++--- 3 files changed, 35 insertions(+), 44 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 9efcac9714..502ed8ff96 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -4,34 +4,19 @@ import click import os import re -import gazu -from gazu.asset import all_assets_for_project, all_asset_types, new_asset -from gazu.shot import ( - all_episodes_for_project, - all_sequences_for_project, - all_shots_for_project, - new_episode, - new_sequence, - new_shot, -) -from gazu.task import ( - all_task_types, - new_task, - new_task_type, -) from pymongo import DeleteOne, UpdateOne from avalon.api import AvalonMongoDB from openpype.api import get_project_settings -from openpype.modules import JsonFilesSettingsDef, OpenPypeModule, ModulesManager -from openpype.modules.kitsu.utils.openpype import ( +from openpype.modules import OpenPypeModule, ModulesManager +from openpype.settings.lib import get_local_settings +from openpype_interfaces import IPluginPaths, ITrayAction +from .utils.listeners import start_listeners +from .utils.openpype import ( create_op_asset, sync_project, update_op_assets, ) -from openpype.settings.lib import get_local_settings -from openpype_interfaces import IPluginPaths, ITrayAction -from .utils.listeners import start_listeners class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): @@ -137,6 +122,7 @@ def cli_main(): @cli_main.command() def sync_zou(): """Synchronize Zou server database (Kitsu backend) with openpype database.""" + import gazu # Connect to server gazu.client.set_host(os.environ["KITSU_SERVER"]) @@ -178,11 +164,11 @@ def sync_zou(): gazu.project.update_project_data(zou_project, data=op_project["data"]) gazu.project.update_project(zou_project) - asset_types = all_asset_types() - all_assets = all_assets_for_project(zou_project) - all_episodes = all_episodes_for_project(zou_project) - all_seqs = all_sequences_for_project(zou_project) - all_shots = all_shots_for_project(zou_project) + asset_types = gazu.asset.all_asset_types() + all_assets = gazu.asset.all_assets_for_project(zou_project) + all_episodes = gazu.shot.all_episodes_for_project(zou_project) + all_seqs = gazu.shot.all_sequences_for_project(zou_project) + all_shots = gazu.shot.all_shots_for_project(zou_project) all_entities_ids = { e["id"] for e in all_episodes + all_seqs + all_shots + all_assets } @@ -217,7 +203,9 @@ def sync_zou(): # Match asset type by it's name match = regex_ep.match(doc["name"]) if not match: # Asset - new_entity = new_asset(zou_project, asset_types[0], doc["name"]) + new_entity = gazu.asset.new_asset( + zou_project, asset_types[0], doc["name"] + ) # Match case in shot Collection: :param dbcon: Connection to DB. :param project_id: Project zou ID """ + import gazu + project = gazu.project.get_project(project_id) project_name = project["name"] dbcon.Session["AVALON_PROJECT"] = project_name @@ -49,6 +45,11 @@ def update_op_assets( :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] :return: List of (doc_id, update_dict) tuples """ + from gazu.task import ( + all_tasks_for_asset, + all_tasks_for_shot, + ) + assets_with_update = [] for item in entities_list: # Update asset From 44100b0da7d30bc34af0635b5e577e2a6df03c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 09:52:26 +0100 Subject: [PATCH 029/194] line length to 79 --- openpype/modules/kitsu/kitsu_module.py | 33 ++++++++++++++----- openpype/modules/kitsu/kitsu_widgets.py | 14 +++++--- .../kitsu/plugins/publish/kitsu_plugin.py | 4 ++- openpype/modules/kitsu/utils/listeners.py | 29 +++++++++++----- openpype/modules/kitsu/utils/openpype.py | 7 ++-- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 502ed8ff96..a7b3b17eb5 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -42,7 +42,9 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): # Check for "/api" url validity if not kitsu_url.endswith("api"): - kitsu_url = f"{kitsu_url}{'' if kitsu_url.endswith('/') else '/'}api" + kitsu_url = ( + f"{kitsu_url}{'' if kitsu_url.endswith('/') else '/'}api" + ) self.server_url = kitsu_url @@ -161,7 +163,9 @@ def sync_zou(): "resolution": f"{op_project['data']['resolutionWidth']}x{op_project['data']['resolutionHeight']}", } ) - gazu.project.update_project_data(zou_project, data=op_project["data"]) + gazu.project.update_project_data( + zou_project, data=op_project["data"] + ) gazu.project.update_project(zou_project) asset_types = gazu.asset.all_asset_types() @@ -227,7 +231,9 @@ def sync_zou(): # Create new sequence and set it as substitute created_sequence = gazu.shot.new_sequence( - zou_project, substitute_sequence_name, episode=zou_parent_id + zou_project, + substitute_sequence_name, + episode=zou_parent_id, ) gazu.shot.update_sequence_data( created_sequence, {"is_substitute": True} @@ -244,13 +250,16 @@ def sync_zou(): 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"], ) elif match.group(2): # Sequence parent_doc = asset_docs[visual_parent_id] new_entity = gazu.shot.new_sequence( - zou_project, doc["name"], episode=parent_doc["data"]["zou"]["id"] + zou_project, + doc["name"], + episode=parent_doc["data"]["zou"]["id"], ) elif match.group(3): # Episode @@ -293,7 +302,10 @@ def sync_zou(): if frame_in or frame_out: entity_data.update( { - "data": {"frame_in": frame_in, "frame_out": frame_out}, + "data": { + "frame_in": frame_in, + "frame_out": frame_out, + }, "nb_frames": frame_out - frame_in, } ) @@ -334,7 +346,10 @@ def sync_zou(): @cli_main.command() @click.option( - "-l", "--listen", is_flag=True, help="Listen Kitsu server after synchronization." + "-l", + "--listen", + is_flag=True, + help="Listen Kitsu server after synchronization.", ) def sync_openpype(listen: bool): """Synchronize openpype database from Zou sever database.""" @@ -410,7 +425,9 @@ def sync_openpype(listen: bool): bulk_writes.extend( [ UpdateOne({"_id": id}, update) - for id, update in update_op_assets(all_entities, zou_ids_and_asset_docs) + for id, update in update_op_assets( + all_entities, zou_ids_and_asset_docs + ) ] ) diff --git a/openpype/modules/kitsu/kitsu_widgets.py b/openpype/modules/kitsu/kitsu_widgets.py index 6bd436f460..1a32182795 100644 --- a/openpype/modules/kitsu/kitsu_widgets.py +++ b/openpype/modules/kitsu/kitsu_widgets.py @@ -46,7 +46,8 @@ class PasswordDialog(QtWidgets.QDialog): login_label = QtWidgets.QLabel("Login:", login_widget) login_input = QtWidgets.QLineEdit( - login_widget, text=kitsu_settings.get("login") if remembered else None + login_widget, + text=kitsu_settings.get("login") if remembered else None, ) login_input.setPlaceholderText("Your Kitsu account login...") @@ -61,7 +62,8 @@ class PasswordDialog(QtWidgets.QDialog): password_label = QtWidgets.QLabel("Password:", password_widget) password_input = QtWidgets.QLineEdit( - password_widget, text=kitsu_settings.get("password") if remembered else None + password_widget, + text=kitsu_settings.get("password") if remembered else None, ) password_input.setPlaceholderText("Your password...") password_input.setEchoMode(QtWidgets.QLineEdit.Password) @@ -87,7 +89,9 @@ class PasswordDialog(QtWidgets.QDialog): remember_checkbox = QtWidgets.QCheckBox("Remember", buttons_widget) remember_checkbox.setObjectName("RememberCheckbox") - remember_checkbox.setChecked(remembered if remembered is not None else True) + remember_checkbox.setChecked( + remembered if remembered is not None else True + ) ok_btn = QtWidgets.QPushButton("Ok", buttons_widget) cancel_btn = QtWidgets.QPushButton("Cancel", buttons_widget) @@ -137,7 +141,9 @@ class PasswordDialog(QtWidgets.QDialog): def _on_ok_click(self): # Check if is connectable if not self._connectable: - self.message_label.setText("Please set server url in Studio Settings!") + self.message_label.setText( + "Please set server url in Studio Settings!" + ) return # Collect values diff --git a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py index 86ba40a5f4..24f1e4e80c 100644 --- a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py +++ b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py @@ -31,7 +31,9 @@ class IntegrateRig(pyblish.api.InstancePlugin): # Get task task_type = gazu.task.get_task_type_by_name(instance.data["task"]) - entity_task = gazu.task.get_task_by_entity(asset_data["zou"], task_type) + entity_task = gazu.task.get_task_by_entity( + asset_data["zou"], task_type + ) # Comment entity gazu.task.add_comment( diff --git a/openpype/modules/kitsu/utils/listeners.py b/openpype/modules/kitsu/utils/listeners.py index 18f67b13e3..3768b4e8e6 100644 --- a/openpype/modules/kitsu/utils/listeners.py +++ b/openpype/modules/kitsu/utils/listeners.py @@ -95,9 +95,9 @@ def start_listeners(): zou_ids_and_asset_docs[asset["project_id"]] = project_doc # Update - asset_doc_id, asset_update = update_op_assets([asset], zou_ids_and_asset_docs)[ - 0 - ] + asset_doc_id, asset_update = update_op_assets( + [asset], zou_ids_and_asset_docs + )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) def delete_asset(data): @@ -105,7 +105,9 @@ def start_listeners(): project_col = set_op_project(dbcon, data["project_id"]) # Delete - project_col.delete_one({"type": "asset", "data.zou.id": data["asset_id"]}) + project_col.delete_one( + {"type": "asset", "data.zou.id": data["asset_id"]} + ) gazu.events.add_listener(event_client, "asset:new", new_asset) gazu.events.add_listener(event_client, "asset:update", update_asset) @@ -155,7 +157,9 @@ def start_listeners(): print("delete episode") # TODO check bugfix # Delete - project_col.delete_one({"type": "asset", "data.zou.id": data["episode_id"]}) + project_col.delete_one( + {"type": "asset", "data.zou.id": data["episode_id"]} + ) gazu.events.add_listener(event_client, "episode:new", new_episode) gazu.events.add_listener(event_client, "episode:update", update_episode) @@ -205,7 +209,9 @@ def start_listeners(): print("delete sequence") # TODO check bugfix # Delete - project_col.delete_one({"type": "asset", "data.zou.id": data["sequence_id"]}) + project_col.delete_one( + {"type": "asset", "data.zou.id": data["sequence_id"]} + ) gazu.events.add_listener(event_client, "sequence:new", new_sequence) gazu.events.add_listener(event_client, "sequence:update", update_sequence) @@ -244,7 +250,9 @@ def start_listeners(): zou_ids_and_asset_docs[shot["project_id"]] = project_doc # Update - asset_doc_id, asset_update = update_op_assets([shot], zou_ids_and_asset_docs)[0] + asset_doc_id, asset_update = update_op_assets( + [shot], zou_ids_and_asset_docs + )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) def delete_shot(data): @@ -252,7 +260,9 @@ def start_listeners(): project_col = set_op_project(dbcon, data["project_id"]) # Delete - project_col.delete_one({"type": "asset", "data.zou.id": data["shot_id"]}) + project_col.delete_one( + {"type": "asset", "data.zou.id": data["shot_id"]} + ) gazu.events.add_listener(event_client, "shot:new", new_shot) gazu.events.add_listener(event_client, "shot:update", update_shot) @@ -302,7 +312,8 @@ def start_listeners(): # Delete task in DB project_col.update_one( - {"_id": doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} + {"_id": doc["_id"]}, + {"$set": {"data.tasks": asset_tasks}}, ) return diff --git a/openpype/modules/kitsu/utils/openpype.py b/openpype/modules/kitsu/utils/openpype.py index 2443323893..8aabba6de0 100644 --- a/openpype/modules/kitsu/utils/openpype.py +++ b/openpype/modules/kitsu/utils/openpype.py @@ -65,7 +65,8 @@ def update_op_assets( tasks_list = all_tasks_for_shot(item) # TODO frame in and out item_data["tasks"] = { - t["task_type_name"]: {"type": t["task_type_name"]} for t in tasks_list + t["task_type_name"]: {"type": t["task_type_name"]} + for t in tasks_list } # Get zou parent id for correct hierarchy @@ -79,7 +80,9 @@ def update_op_assets( parent_zou_id = substitute_parent_item["parent_id"] else: parent_zou_id = ( - item.get("parent_id") or item.get("episode_id") or item.get("source_id") + item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # TODO check consistency # Visual parent for hierarchy From c26c2f09a8f271419e76cd7407f35b19d579d851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 09:54:47 +0100 Subject: [PATCH 030/194] fix import --- openpype/modules/kitsu/utils/openpype.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/kitsu/utils/openpype.py b/openpype/modules/kitsu/utils/openpype.py index 8aabba6de0..56c99effff 100644 --- a/openpype/modules/kitsu/utils/openpype.py +++ b/openpype/modules/kitsu/utils/openpype.py @@ -132,6 +132,8 @@ def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: :param dbcon: DB to create project in :return: Update instance for the project """ + import gazu + project_name = project["name"] project_doc = dbcon.find_one({"type": "project"}) if not project_doc: From e8831947e6c2de902470e0fffdb69cad6429ce32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 09:57:00 +0100 Subject: [PATCH 031/194] Fix __all__ --- openpype/modules/kitsu/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/__init__.py b/openpype/modules/kitsu/__init__.py index 6cb62bbb15..9737a054f6 100644 --- a/openpype/modules/kitsu/__init__.py +++ b/openpype/modules/kitsu/__init__.py @@ -6,4 +6,4 @@ be found by OpenPype discovery. from .kitsu_module import KitsuModule -__all__ = "KitsuModule" +__all__ = ("KitsuModule", ) From cde925c09b06130f6b4e1f36d993d21aba0fd7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 11:38:47 +0100 Subject: [PATCH 032/194] Use OpenPypeSecureRegistry for authentication --- openpype/modules/kitsu/__init__.py | 2 +- openpype/modules/kitsu/kitsu_module.py | 22 ++++++++++++++--- openpype/modules/kitsu/kitsu_widgets.py | 33 +++++++++++-------------- 3 files changed, 33 insertions(+), 24 deletions(-) diff --git a/openpype/modules/kitsu/__init__.py b/openpype/modules/kitsu/__init__.py index 9737a054f6..9220cb1762 100644 --- a/openpype/modules/kitsu/__init__.py +++ b/openpype/modules/kitsu/__init__.py @@ -6,4 +6,4 @@ be found by OpenPype discovery. from .kitsu_module import KitsuModule -__all__ = ("KitsuModule", ) +__all__ = ("KitsuModule",) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index a7b3b17eb5..ebfa0dbeea 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -8,6 +8,7 @@ from pymongo import DeleteOne, UpdateOne from avalon.api import AvalonMongoDB from openpype.api import get_project_settings +from openpype.lib.local_settings import OpenPypeSecureRegistry from openpype.modules import OpenPypeModule, ModulesManager from openpype.settings.lib import get_local_settings from openpype_interfaces import IPluginPaths, ITrayAction @@ -28,7 +29,9 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): def initialize(self, settings): """Initialization of module.""" module_settings = settings[self.name] - local_kitsu_settings = get_local_settings().get("kitsu", {}) + + # Get user registry + user_registry = OpenPypeSecureRegistry("kitsu_user") # Enabled by settings self.enabled = module_settings.get("enabled", False) @@ -49,8 +52,8 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): self.server_url = kitsu_url # Set credentials - self.kitsu_login = local_kitsu_settings["login"] - self.kitsu_password = local_kitsu_settings["password"] + self.kitsu_login = user_registry.get_item("login", None) + self.kitsu_password = user_registry.get_item("password", None) # Prepare variables that can be used or set afterwards self._connected_modules = None @@ -359,7 +362,13 @@ def sync_openpype(listen: bool): gazu.client.set_host(os.environ["KITSU_SERVER"]) # Authenticate - gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) + kitsu_login = os.environ.get("KITSU_LOGIN") + kitsu_pwd = os.environ.get("KITSU_PWD") + if not kitsu_login or not kitsu_pwd: # Sentinel to log-in + log_in_dialog() + return + + gazu.log_in(kitsu_login, kitsu_pwd) # Iterate projects dbcon = AvalonMongoDB() @@ -462,6 +471,11 @@ def listen(): @cli_main.command() def sign_in(): + """Sign-in command.""" + log_in_dialog() + + +def log_in_dialog(): """Show credentials dialog.""" from openpype.tools.utils.lib import qt_app_context diff --git a/openpype/modules/kitsu/kitsu_widgets.py b/openpype/modules/kitsu/kitsu_widgets.py index 1a32182795..1a48e6dbc0 100644 --- a/openpype/modules/kitsu/kitsu_widgets.py +++ b/openpype/modules/kitsu/kitsu_widgets.py @@ -4,11 +4,10 @@ import gazu from Qt import QtWidgets, QtCore, QtGui from openpype import style +from openpype.lib.local_settings import OpenPypeSecureRegistry from openpype.resources import get_resource from openpype.settings.lib import ( - get_local_settings, get_system_settings, - save_local_settings, ) from openpype.widgets.password_dialog import PressHoverButton @@ -26,8 +25,11 @@ class PasswordDialog(QtWidgets.QDialog): self.resize(300, 120) system_settings = get_system_settings() - kitsu_settings = get_local_settings().get("kitsu", {}) - remembered = kitsu_settings.get("remember") + user_registry = OpenPypeSecureRegistry("kitsu_user") + remembered = bool( + user_registry.get_item("login", None) + or user_registry.get_item("password", None) + ) self._final_result = None self._connectable = bool( @@ -47,7 +49,7 @@ class PasswordDialog(QtWidgets.QDialog): login_input = QtWidgets.QLineEdit( login_widget, - text=kitsu_settings.get("login") if remembered else None, + text=user_registry.get_item("login") if remembered else None, ) login_input.setPlaceholderText("Your Kitsu account login...") @@ -63,7 +65,7 @@ class PasswordDialog(QtWidgets.QDialog): password_input = QtWidgets.QLineEdit( password_widget, - text=kitsu_settings.get("password") if remembered else None, + text=user_registry.get_item("password") if remembered else None, ) password_input.setPlaceholderText("Your password...") password_input.setEchoMode(QtWidgets.QLineEdit.Password) @@ -161,30 +163,23 @@ class PasswordDialog(QtWidgets.QDialog): os.environ["KITSU_LOGIN"] = login_value os.environ["KITSU_PWD"] = pwd_value - # Get settings - local_settings = get_local_settings() - local_settings.setdefault("kitsu", {}) + # Get user registry + user_registry = OpenPypeSecureRegistry("kitsu_user") # Remember password cases if remember: # Set local settings - local_settings["kitsu"]["login"] = login_value - local_settings["kitsu"]["password"] = pwd_value + user_registry.set_item("login", login_value) + user_registry.set_item("password", pwd_value) else: # Clear local settings - local_settings["kitsu"]["login"] = None - local_settings["kitsu"]["password"] = None + user_registry.delete_item("login") + user_registry.delete_item("password") # Clear input fields self.login_input.clear() self.password_input.clear() - # Keep 'remember' parameter - local_settings["kitsu"]["remember"] = remember - - # Save settings - save_local_settings(local_settings) - self._final_result = True self.close() From c9ea0b3bb262484b43ebd5c1c5d649dcdaf75d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 2 Mar 2022 11:49:49 +0100 Subject: [PATCH 033/194] Line length max 79 --- openpype/modules/kitsu/kitsu_module.py | 28 +++++++++++++++++-------- openpype/modules/kitsu/kitsu_widgets.py | 7 ++++++- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index ebfa0dbeea..6a2e517832 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -10,7 +10,6 @@ from avalon.api import AvalonMongoDB from openpype.api import get_project_settings from openpype.lib.local_settings import OpenPypeSecureRegistry from openpype.modules import OpenPypeModule, ModulesManager -from openpype.settings.lib import get_local_settings from openpype_interfaces import IPluginPaths, ITrayAction from .utils.listeners import start_listeners from .utils.openpype import ( @@ -126,7 +125,7 @@ def cli_main(): @cli_main.command() def sync_zou(): - """Synchronize Zou server database (Kitsu backend) with openpype database.""" + """Synchronize Zou database (Kitsu backend) with openpype database.""" import gazu # Connect to server @@ -154,7 +153,9 @@ def sync_zou(): # Create project if zou_project is None: raise RuntimeError( - f"Project '{project_name}' doesn't exist in Zou database, please create it in Kitsu and add OpenPype user to it before running synchronization." + f"Project '{project_name}' doesn't exist in Zou database, " + "please create it in Kitsu and add OpenPype user to it before " + "running synchronization." ) # Update project settings and data @@ -163,7 +164,8 @@ def sync_zou(): { "code": op_project["data"]["code"], "fps": op_project["data"]["fps"], - "resolution": f"{op_project['data']['resolutionWidth']}x{op_project['data']['resolutionHeight']}", + "resolution": f"{op_project['data']['resolutionWidth']}" + f"x{op_project['data']['resolutionHeight']}", } ) gazu.project.update_project_data( @@ -213,7 +215,8 @@ def sync_zou(): new_entity = gazu.asset.new_asset( zou_project, asset_types[0], doc["name"] ) - # Match case in shot Date: Wed, 2 Mar 2022 12:11:05 +0100 Subject: [PATCH 034/194] Rename module to Kitsu Connect --- openpype/modules/kitsu/kitsu_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 6a2e517832..d5e744ceb5 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -22,7 +22,7 @@ from .utils.openpype import ( class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Kitsu module class.""" - label = "Kitsu" + label = "Kitsu Connect" name = "kitsu" def initialize(self, settings): From c39bdee4330b1578cfe821d0506dbc7b59373621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 3 Mar 2022 15:50:35 +0100 Subject: [PATCH 035/194] Refactor for login system following recommendations. --- openpype/modules/kitsu/kitsu_module.py | 457 ++---------------- openpype/modules/kitsu/kitsu_widgets.py | 59 +-- .../kitsu/plugins/publish/kitsu_plugin.py | 2 +- openpype/modules/kitsu/utils/credentials.py | 92 ++++ .../utils/{listeners.py => sync_service.py} | 238 +++++---- .../{openpype.py => update_op_with_zou.py} | 153 +++++- .../modules/kitsu/utils/update_zou_with_op.py | 262 ++++++++++ 7 files changed, 717 insertions(+), 546 deletions(-) create mode 100644 openpype/modules/kitsu/utils/credentials.py rename openpype/modules/kitsu/utils/{listeners.py => sync_service.py} (54%) rename openpype/modules/kitsu/utils/{openpype.py => update_op_with_zou.py} (52%) create mode 100644 openpype/modules/kitsu/utils/update_zou_with_op.py diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index d5e744ceb5..dca6133e88 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -2,21 +2,9 @@ import click import os -import re -from pymongo import DeleteOne, UpdateOne - -from avalon.api import AvalonMongoDB -from openpype.api import get_project_settings -from openpype.lib.local_settings import OpenPypeSecureRegistry -from openpype.modules import OpenPypeModule, ModulesManager +from openpype.modules import OpenPypeModule from openpype_interfaces import IPluginPaths, ITrayAction -from .utils.listeners import start_listeners -from .utils.openpype import ( - create_op_asset, - sync_project, - update_op_assets, -) class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): @@ -29,9 +17,6 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): """Initialization of module.""" module_settings = settings[self.name] - # Get user registry - user_registry = OpenPypeSecureRegistry("kitsu_user") - # Enabled by settings self.enabled = module_settings.get("enabled", False) @@ -44,66 +29,58 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): # Check for "/api" url validity if not kitsu_url.endswith("api"): - kitsu_url = ( - f"{kitsu_url}{'' if kitsu_url.endswith('/') else '/'}api" + kitsu_url = "{}{}api".format( + kitsu_url, "" if kitsu_url.endswith("/") else "/" ) self.server_url = kitsu_url - # Set credentials - self.kitsu_login = user_registry.get_item("login", None) - self.kitsu_password = user_registry.get_item("password", None) - - # Prepare variables that can be used or set afterwards - self._connected_modules = None # UI which must not be created at this time self._dialog = None def tray_init(self): - """Implementation of abstract method for `ITrayAction`. - - We're definitely in tray tool so we can pre create dialog. - """ + """Tray init.""" self._create_dialog() + def tray_start(self): + """Tray start.""" + from .utils.credentials import ( + load_credentials, + validate_credentials, + set_credentials_envs, + ) + + username, password = load_credentials() + + # Check credentials, ask them if needed + if validate_credentials(username, password): + set_credentials_envs(username, password) + else: + self.show_dialog() + def get_global_environments(self): """Kitsu's global environments.""" - return { - "KITSU_SERVER": self.server_url, - "KITSU_LOGIN": self.kitsu_login, - "KITSU_PWD": self.kitsu_password, - } + return {"KITSU_SERVER": self.server_url} def _create_dialog(self): # Don't recreate dialog if already exists if self._dialog is not None: return - from .kitsu_widgets import PasswordDialog + from .kitsu_widgets import KitsuPasswordDialog - self._dialog = PasswordDialog() + self._dialog = KitsuPasswordDialog() def show_dialog(self): - """Show dialog with connected modules. + """Show dialog to log-in.""" - This can be called from anywhere but can also crash in headless mode. - There is no way to prevent addon to do invalid operations if he's - not handling them. - """ # Make sure dialog is created self._create_dialog() + # Show dialog self._dialog.open() - def get_connected_modules(self): - """Custom implementation of addon.""" - names = set() - if self._connected_modules is not None: - for module in self._connected_modules: - names.add(module.name) - return names - def on_action_trigger(self): """Implementation of abstract method for `ITrayAction`.""" self.show_dialog() @@ -124,372 +101,36 @@ def cli_main(): @cli_main.command() -def sync_zou(): - """Synchronize Zou database (Kitsu backend) with openpype database.""" - import gazu - - # Connect to server - gazu.client.set_host(os.environ["KITSU_SERVER"]) - - # Authenticate - gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) - - # Iterate projects - dbcon = AvalonMongoDB() - dbcon.install() - - op_projects = [p for p in dbcon.projects()] - bulk_writes = [] - for op_project in op_projects: - # Create project locally - # Try to find project document - project_name = op_project["name"] - dbcon.Session["AVALON_PROJECT"] = project_name - - # Get all entities from zou - print(f"Synchronizing {project_name}...") - zou_project = gazu.project.get_project_by_name(project_name) - - # Create project - if zou_project is None: - raise RuntimeError( - f"Project '{project_name}' doesn't exist in Zou database, " - "please create it in Kitsu and add OpenPype user to it before " - "running synchronization." - ) - - # Update project settings and data - if op_project["data"]: - zou_project.update( - { - "code": op_project["data"]["code"], - "fps": op_project["data"]["fps"], - "resolution": f"{op_project['data']['resolutionWidth']}" - f"x{op_project['data']['resolutionHeight']}", - } - ) - gazu.project.update_project_data( - zou_project, data=op_project["data"] - ) - gazu.project.update_project(zou_project) - - asset_types = gazu.asset.all_asset_types() - all_assets = gazu.asset.all_assets_for_project(zou_project) - all_episodes = gazu.shot.all_episodes_for_project(zou_project) - all_seqs = gazu.shot.all_sequences_for_project(zou_project) - all_shots = gazu.shot.all_shots_for_project(zou_project) - all_entities_ids = { - e["id"] for e in all_episodes + all_seqs + all_shots + all_assets - } - - # Query all assets of the local project - project_module_settings = get_project_settings(project_name)["kitsu"] - project_col = dbcon.database[project_name] - asset_docs = { - asset_doc["_id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) - } - - # Create new assets - new_assets_docs = [ - doc - for doc in asset_docs.values() - if doc["data"].get("zou", {}).get("id") not in all_entities_ids - ] - naming_pattern = project_module_settings["entities_naming_pattern"] - regex_ep = re.compile( - r"(.*{}.*)|(.*{}.*)|(.*{}.*)".format( - naming_pattern["shot"].replace("#", ""), - naming_pattern["sequence"].replace("#", ""), - naming_pattern["episode"].replace("#", ""), - ), - re.IGNORECASE, - ) - for doc in new_assets_docs: - visual_parent_id = doc["data"]["visualParent"] - parent_substitutes = [] - - # Match asset type by it's name - match = regex_ep.match(doc["name"]) - if not match: # Asset - new_entity = gazu.asset.new_asset( - zou_project, asset_types[0], doc["name"] - ) - # Match case in shot bool: + """Validate credentials by trying to connect to Kitsu host URL. + + :param login: Kitsu Login + :param password: Kitsu Password + :param kitsu_url: Kitsu host URL + :return: Are credentials valid? + """ + # Connect to server + validate_host(kitsu_url) + + # Authenticate + try: + gazu.log_in(login, password) + except gazu.exception.AuthFailedException: + return False + + return True + + +def validate_host(kitsu_url: str) -> bool: + """Validate credentials by trying to connect to Kitsu host URL. + + :param kitsu_url: Kitsu host URL + :return: Is host valid? + """ + # Connect to server + gazu.set_host(kitsu_url) + + # Test host + if gazu.client.host_is_valid(): + return True + else: + raise gazu.exception.HostException(f"Host '{kitsu_url}' is invalid.") + + +def clear_credentials(): + """Clear credentials in Secure Registry.""" + # Get user registry + user_registry = OpenPypeSecureRegistry("kitsu_user") + + # Set local settings + user_registry.delete_item("login") + user_registry.delete_item("password") + + +def save_credentials(login: str, password: str): + """Save credentials in Secure Registry. + + :param login: Kitsu Login + :param password: Kitsu Password + """ + # Get user registry + user_registry = OpenPypeSecureRegistry("kitsu_user") + + # Set local settings + user_registry.set_item("login", login) + user_registry.set_item("password", password) + + +def load_credentials() -> Tuple[str, str]: + """Load registered credentials. + + :return: Login, Password + """ + # Get user registry + user_registry = OpenPypeSecureRegistry("kitsu_user") + + return user_registry.get_item("login", None), user_registry.get_item( + "password", None + ) + + +def set_credentials_envs(login: str, password: str): + """Set environment variables with Kitsu login and password. + + :param login: Kitsu Login + :param password: Kitsu Password + """ + os.environ["KITSU_LOGIN"] = login + os.environ["KITSU_PWD"] = password diff --git a/openpype/modules/kitsu/utils/listeners.py b/openpype/modules/kitsu/utils/sync_service.py similarity index 54% rename from openpype/modules/kitsu/utils/listeners.py rename to openpype/modules/kitsu/utils/sync_service.py index 3768b4e8e6..831673ec0d 100644 --- a/openpype/modules/kitsu/utils/listeners.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -1,72 +1,143 @@ import os +import gazu + from avalon.api import AvalonMongoDB -from .openpype import ( +from .credentials import load_credentials, validate_credentials +from .update_op_with_zou import ( create_op_asset, set_op_project, - sync_project, + write_project_to_op, update_op_assets, ) -def start_listeners(): - """Start listeners to keep OpenPype up-to-date with Kitsu.""" - import gazu +class Listener: + """Host Kitsu listener.""" - # Connect to server - gazu.client.set_host(os.environ["KITSU_SERVER"]) + def __init__(self, login, password): + """Create client and add listeners to events without starting it. - # Authenticate - gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) - gazu.set_event_host(os.environ["KITSU_SERVER"].replace("api", "socket.io")) - event_client = gazu.events.init() + Run `listener.start()` to actually start the service. - # Connect to DB - dbcon = AvalonMongoDB() - dbcon.install() + Args: + login (str): Kitsu user login + password (str): Kitsu user password + + Raises: + AuthFailedException: Wrong user login and/or password + """ + self.dbcon = AvalonMongoDB() + self.dbcon.install() + + 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}'..." + ) + + gazu.set_event_host( + os.environ["KITSU_SERVER"].replace("api", "socket.io") + ) + self.event_client = gazu.events.init() + + gazu.events.add_listener( + self.event_client, "project:new", self._new_project + ) + gazu.events.add_listener( + self.event_client, "project:update", self._update_project + ) + gazu.events.add_listener( + self.event_client, "project:delete", self._delete_project + ) + + gazu.events.add_listener( + self.event_client, "asset:new", self._new_asset + ) + gazu.events.add_listener( + self.event_client, "asset:update", self._update_asset + ) + gazu.events.add_listener( + self.event_client, "asset:delete", self._delete_asset + ) + + gazu.events.add_listener( + self.event_client, "episode:new", self._new_episode + ) + gazu.events.add_listener( + self.event_client, "episode:update", self._update_episode + ) + gazu.events.add_listener( + self.event_client, "episode:delete", self._delete_episode + ) + + gazu.events.add_listener( + self.event_client, "sequence:new", self._new_sequence + ) + gazu.events.add_listener( + self.event_client, "sequence:update", self._update_sequence + ) + gazu.events.add_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:update", self._update_shot + ) + gazu.events.add_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:update", self._update_task + ) + gazu.events.add_listener( + self.event_client, "task:delete", self._delete_task + ) + + def start(self): + gazu.events.run_client(self.event_client) # == Project == - - def new_project(data): + def _new_project(self, data): """Create new project into OP DB.""" # Use update process to avoid duplicating code - update_project(data) + self._update_project(data) - def update_project(data): + 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"] - dbcon.Session["AVALON_PROJECT"] = project_name - update_project = sync_project(project, dbcon) + update_project = write_project_to_op(project, self.dbcon) # Write into DB if update_project: - project_col = dbcon.database[project_name] + project_col = self.dbcon.database[project_name] project_col.bulk_write([update_project]) - def delete_project(data): + def _delete_project(self, data): """Delete project.""" # Get project entity print(data) # TODO check bugfix project = gazu.project.get_project(data["project_id"]) # Delete project collection - project_col = dbcon.database[project["name"]] + project_col = self.dbcon.database[project["name"]] project_col.drop() - gazu.events.add_listener(event_client, "project:new", new_project) - gazu.events.add_listener(event_client, "project:update", update_project) - gazu.events.add_listener(event_client, "project:delete", delete_project) - # == Asset == - def new_asset(data): + def _new_asset(self, data): """Create new asset into OP DB.""" # Get project entity - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Get gazu entity asset = gazu.asset.get_asset(data["asset_id"]) @@ -75,12 +146,12 @@ def start_listeners(): project_col.insert_one(create_op_asset(asset)) # Update - update_asset(data) + self._update_asset(data) - def update_asset(data): + def _update_asset(self, data): """Update asset into OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) - project_doc = dbcon.find_one({"type": "project"}) + project_col = set_op_project(self.dbcon, data["project_id"]) + project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity asset = gazu.asset.get_asset(data["asset_id"]) @@ -100,24 +171,20 @@ def start_listeners(): )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) - def delete_asset(data): + def _delete_asset(self, data): """Delete asset of OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Delete project_col.delete_one( {"type": "asset", "data.zou.id": data["asset_id"]} ) - gazu.events.add_listener(event_client, "asset:new", new_asset) - gazu.events.add_listener(event_client, "asset:update", update_asset) - gazu.events.add_listener(event_client, "asset:delete", delete_asset) - # == Episode == - def new_episode(data): + def _new_episode(self, data): """Create new episode into OP DB.""" # Get project entity - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Get gazu entity episode = gazu.shot.get_episode(data["episode_id"]) @@ -126,12 +193,12 @@ def start_listeners(): project_col.insert_one(create_op_asset(episode)) # Update - update_episode(data) + self._update_episode(data) - def update_episode(data): + def _update_episode(self, data): """Update episode into OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) - project_doc = dbcon.find_one({"type": "project"}) + project_col = set_op_project(self.dbcon, data["project_id"]) + project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity episode = gazu.shot.get_episode(data["episode_id"]) @@ -151,9 +218,9 @@ def start_listeners(): )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) - def delete_episode(data): + def _delete_episode(self, data): """Delete shot of OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) print("delete episode") # TODO check bugfix # Delete @@ -161,15 +228,11 @@ def start_listeners(): {"type": "asset", "data.zou.id": data["episode_id"]} ) - gazu.events.add_listener(event_client, "episode:new", new_episode) - gazu.events.add_listener(event_client, "episode:update", update_episode) - gazu.events.add_listener(event_client, "episode:delete", delete_episode) - # == Sequence == - def new_sequence(data): + def _new_sequence(self, data): """Create new sequnce into OP DB.""" # Get project entity - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Get gazu entity sequence = gazu.shot.get_sequence(data["sequence_id"]) @@ -178,12 +241,12 @@ def start_listeners(): project_col.insert_one(create_op_asset(sequence)) # Update - update_sequence(data) + self._update_sequence(data) - def update_sequence(data): + def _update_sequence(self, data): """Update sequence into OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) - project_doc = dbcon.find_one({"type": "project"}) + project_col = set_op_project(self.dbcon, data["project_id"]) + project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity sequence = gazu.shot.get_sequence(data["sequence_id"]) @@ -203,9 +266,9 @@ def start_listeners(): )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) - def delete_sequence(data): + def _delete_sequence(self, data): """Delete sequence of OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) print("delete sequence") # TODO check bugfix # Delete @@ -213,15 +276,11 @@ def start_listeners(): {"type": "asset", "data.zou.id": data["sequence_id"]} ) - gazu.events.add_listener(event_client, "sequence:new", new_sequence) - gazu.events.add_listener(event_client, "sequence:update", update_sequence) - gazu.events.add_listener(event_client, "sequence:delete", delete_sequence) - # == Shot == - def new_shot(data): + def _new_shot(self, data): """Create new shot into OP DB.""" # Get project entity - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Get gazu entity shot = gazu.shot.get_shot(data["shot_id"]) @@ -230,12 +289,12 @@ def start_listeners(): project_col.insert_one(create_op_asset(shot)) # Update - update_shot(data) + self._update_shot(data) - def update_shot(data): + def _update_shot(self, data): """Update shot into OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) - project_doc = dbcon.find_one({"type": "project"}) + project_col = set_op_project(self.dbcon, data["project_id"]) + project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity shot = gazu.shot.get_shot(data["shot_id"]) @@ -255,25 +314,20 @@ def start_listeners(): )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) - def delete_shot(data): + def _delete_shot(self, data): """Delete shot of OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Delete project_col.delete_one( {"type": "asset", "data.zou.id": data["shot_id"]} ) - gazu.events.add_listener(event_client, "shot:new", new_shot) - gazu.events.add_listener(event_client, "shot:update", update_shot) - gazu.events.add_listener(event_client, "shot:delete", delete_shot) - # == Task == - def new_task(data): + def _new_task(self, data): """Create new task into OP DB.""" - print("new", data) # Get project entity - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Get gazu entity task = gazu.task.get_task(data["task_id"]) @@ -291,14 +345,14 @@ def start_listeners(): {"_id": asset_doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} ) - def update_task(data): + def _update_task(self, data): """Update task into OP DB.""" # TODO is it necessary? pass - def delete_task(data): + def _delete_task(self, data): """Delete task of OP DB.""" - project_col = set_op_project(dbcon, data["project_id"]) + project_col = set_op_project(self.dbcon, data["project_id"]) # Find asset doc asset_docs = [doc for doc in project_col.find({"type": "asset"})] @@ -307,7 +361,7 @@ def start_listeners(): 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 @@ -317,8 +371,20 @@ def start_listeners(): ) return - gazu.events.add_listener(event_client, "task:new", new_task) - gazu.events.add_listener(event_client, "task:update", update_task) - gazu.events.add_listener(event_client, "task:delete", delete_task) - gazu.events.run_client(event_client) +def start_listeners(login: str, password: str): + """Start listeners to keep OpenPype up-to-date with Kitsu. + + Args: + login (str): Kitsu user login + password (str): Kitsu user password + """ + + # Connect to server + listener = Listener(login, password) + listener.start() + + +if __name__ == "__main__": + # TODO not sure when this can be run and if this system is reliable + start_listeners(load_credentials()) diff --git a/openpype/modules/kitsu/utils/openpype.py b/openpype/modules/kitsu/utils/update_op_with_zou.py similarity index 52% rename from openpype/modules/kitsu/utils/openpype.py rename to openpype/modules/kitsu/utils/update_op_with_zou.py index 56c99effff..eb675ad09e 100644 --- a/openpype/modules/kitsu/utils/openpype.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -1,10 +1,17 @@ +"""Functions to update OpenPype data using Kitsu DB (a.k.a Zou).""" from typing import Dict, List -from pymongo import UpdateOne +from pymongo import DeleteOne, UpdateOne from pymongo.collection import Collection +import gazu +from gazu.task import ( + all_tasks_for_asset, + all_tasks_for_shot, +) from avalon.api import AvalonMongoDB from openpype.lib import create_project +from openpype.modules.kitsu.utils.credentials import validate_credentials def create_op_asset(gazu_entity: dict) -> dict: @@ -26,8 +33,6 @@ def set_op_project(dbcon, project_id) -> Collection: :param dbcon: Connection to DB. :param project_id: Project zou ID """ - import gazu - project = gazu.project.get_project(project_id) project_name = project["name"] dbcon.Session["AVALON_PROJECT"] = project_name @@ -45,11 +50,6 @@ def update_op_assets( :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] :return: List of (doc_id, update_dict) tuples """ - from gazu.task import ( - all_tasks_for_asset, - all_tasks_for_shot, - ) - assets_with_update = [] for item in entities_list: # Update asset @@ -124,18 +124,19 @@ def update_op_assets( return assets_with_update -def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: - """Sync project with database. +def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: + """Write gazu project to OP database. Create project if doesn't exist. - :param project: Gazu project - :param dbcon: DB to create project in - :return: Update instance for the project - """ - import gazu + Args: + project (dict): Gazu project + dbcon (AvalonMongoDB): DB to create project in + Returns: + UpdateOne: Update instance for the project + """ project_name = project["name"] - project_doc = dbcon.find_one({"type": "project"}) + project_doc = dbcon.database[project_name].find_one({"type": "project"}) if not project_doc: print(f"Creating project '{project_name}'") project_doc = create_project(project_name, project_name, dbcon=dbcon) @@ -165,3 +166,123 @@ def sync_project(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: } }, ) + + +def sync_all_project(login: str, password: str): + """Update all OP projects in DB with Zou data. + + Args: + login (str): Kitsu user login + password (str): Kitsu user password + + Raises: + gazu.exception.AuthFailedException: Wrong user login and/or password + """ + + # Authenticate + if not validate_credentials(login, password): + raise gazu.exception.AuthFailedException( + f"Kitsu authentication failed for login: '{login}'..." + ) + + # Iterate projects + dbcon = AvalonMongoDB() + dbcon.install() + all_projects = gazu.project.all_projects() + for project in all_projects: + sync_project_from_kitsu(project["name"], dbcon, project) + + +def sync_project_from_kitsu( + project_name: str, dbcon: AvalonMongoDB, project: dict = None +): + """Update OP project in DB with Zou data. + + Args: + project_name (str): Name of project to sync + dbcon (AvalonMongoDB): MongoDB connection + project (dict, optional): Project dict got using gazu. + Defaults to None. + """ + bulk_writes = [] + + # Get project from zou + if not project: + project = gazu.project.get_project_by_name(project_name) + project_code = project_name + + # Try to find project document + project_col = dbcon.database[project_code] + project_doc = project_col.find_one({"type": "project"}) + + print(f"Synchronizing {project_name}...") + + # Get all assets from zou + all_assets = gazu.asset.all_assets_for_project(project) + all_episodes = gazu.shot.all_episodes_for_project(project) + all_seqs = gazu.shot.all_sequences_for_project(project) + all_shots = gazu.shot.all_shots_for_project(project) + all_entities = [ + e + for e in all_assets + all_episodes + all_seqs + all_shots + if e["data"] and not e["data"].get("is_substitute") + ] + + # Sync project. Create if doesn't exist + bulk_writes.append(write_project_to_op(project, dbcon)) + + # Query all assets of the local project + zou_ids_and_asset_docs = { + asset_doc["data"]["zou"]["id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou", {}).get("id") + } + zou_ids_and_asset_docs[project["id"]] = project_doc + + # Create + to_insert = [] + to_insert.extend( + [ + create_op_asset(item) + for item in all_entities + if item["id"] not in zou_ids_and_asset_docs.keys() + ] + ) + if to_insert: + # Insert doc in DB + project_col.insert_many(to_insert) + + # Update existing docs + zou_ids_and_asset_docs.update( + { + asset_doc["data"]["zou"]["id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + if asset_doc["data"].get("zou") + } + ) + + # Update + bulk_writes.extend( + [ + UpdateOne({"_id": id}, update) + for id, update in update_op_assets( + all_entities, zou_ids_and_asset_docs + ) + ] + ) + + # Delete + diff_assets = set(zou_ids_and_asset_docs.keys()) - { + e["id"] for e in all_entities + [project] + } + if diff_assets: + bulk_writes.extend( + [ + DeleteOne(zou_ids_and_asset_docs[asset_id]) + for asset_id in diff_assets + ] + ) + + # Write into DB + if bulk_writes: + project_col.bulk_write(bulk_writes) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py new file mode 100644 index 0000000000..d1fcde5601 --- /dev/null +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -0,0 +1,262 @@ +"""Functions to update Kitsu DB (a.k.a Zou) using OpenPype Data.""" + +import re +from typing import List + +import gazu +from pymongo import UpdateOne + +from avalon.api import AvalonMongoDB +from openpype.api import get_project_settings +from openpype.modules.kitsu.utils.credentials import validate_credentials + + +def sync_zou(login: str, password: str): + """Synchronize Zou database (Kitsu backend) with openpype database. + This is an utility function to help updating zou data with OP's, it may not + handle correctly all cases, a human intervention might + be required after all. + Will work better if OP DB has been previously synchronized from zou/kitsu. + + Args: + login (str): Kitsu user login + password (str): Kitsu user password + + Raises: + gazu.exception.AuthFailedException: Wrong user login and/or password + """ + + # Authenticate + if not validate_credentials(login, password): + raise gazu.exception.AuthFailedException( + f"Kitsu authentication failed for login: '{login}'..." + ) + + # Iterate projects + dbcon = AvalonMongoDB() + dbcon.install() + + op_projects = [p for p in dbcon.projects()] + for project_doc in op_projects: + sync_zou_from_op_project(project_doc["name"], dbcon, project_doc) + + +def sync_zou_from_op_project( + project_name: str, dbcon: AvalonMongoDB, project_doc: dict = None +) -> List[UpdateOne]: + """Update OP project in DB with Zou data. + + Args: + project_name (str): Name of project to sync + dbcon (AvalonMongoDB): MongoDB connection + project_doc (str, optional): Project doc to sync + """ + # Get project doc if not provided + if not project_doc: + project_doc = dbcon.database[project_name].find_one( + {"type": "project"} + ) + + # Get all entities from zou + print(f"Synchronizing {project_name}...") + zou_project = gazu.project.get_project_by_name(project_name) + + # Create project + if zou_project is None: + raise RuntimeError( + f"Project '{project_name}' doesn't exist in Zou database, " + "please create it in Kitsu and add OpenPype user to it before " + "running synchronization." + ) + + # Update project settings and data + if project_doc["data"]: + zou_project.update( + { + "code": project_doc["data"]["code"], + "fps": project_doc["data"]["fps"], + "resolution": f"{project_doc['data']['resolutionWidth']}" + f"x{project_doc['data']['resolutionHeight']}", + } + ) + gazu.project.update_project_data(zou_project, data=project_doc["data"]) + gazu.project.update_project(zou_project) + + asset_types = gazu.asset.all_asset_types() + all_assets = gazu.asset.all_assets_for_project(zou_project) + all_episodes = gazu.shot.all_episodes_for_project(zou_project) + all_seqs = gazu.shot.all_sequences_for_project(zou_project) + all_shots = gazu.shot.all_shots_for_project(zou_project) + all_entities_ids = { + e["id"] for e in all_episodes + all_seqs + all_shots + all_assets + } + + # Query all assets of the local project + project_module_settings = get_project_settings(project_name)["kitsu"] + project_col = dbcon.database[project_name] + asset_docs = { + asset_doc["_id"]: asset_doc + for asset_doc in project_col.find({"type": "asset"}) + } + + # Create new assets + new_assets_docs = [ + doc + for doc in asset_docs.values() + if doc["data"].get("zou", {}).get("id") not in all_entities_ids + ] + naming_pattern = project_module_settings["entities_naming_pattern"] + regex_ep = re.compile( + r"(.*{}.*)|(.*{}.*)|(.*{}.*)".format( + naming_pattern["shot"].replace("#", ""), + naming_pattern["sequence"].replace("#", ""), + naming_pattern["episode"].replace("#", ""), + ), + re.IGNORECASE, + ) + bulk_writes = [] + for doc in new_assets_docs: + visual_parent_id = doc["data"]["visualParent"] + parent_substitutes = [] + + # Match asset type by it's name + match = regex_ep.match(doc["name"]) + if not match: # Asset + new_entity = gazu.asset.new_asset( + zou_project, asset_types[0], doc["name"] + ) + # Match case in shot Date: Thu, 3 Mar 2022 15:55:56 +0100 Subject: [PATCH 036/194] Cleaning. --- openpype/modules/kitsu/utils/credentials.py | 36 ++++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/openpype/modules/kitsu/utils/credentials.py b/openpype/modules/kitsu/utils/credentials.py index b4dd5ee4a2..0529380d6d 100644 --- a/openpype/modules/kitsu/utils/credentials.py +++ b/openpype/modules/kitsu/utils/credentials.py @@ -8,15 +8,21 @@ from openpype.lib.local_settings import OpenPypeSecureRegistry def validate_credentials( - login: str, password: str, kitsu_url: str = os.environ.get("KITSU_SERVER") + login: str, password: str, kitsu_url: str = None ) -> bool: """Validate credentials by trying to connect to Kitsu host URL. - :param login: Kitsu Login - :param password: Kitsu Password - :param kitsu_url: Kitsu host URL - :return: Are credentials valid? + Args: + login (str): Kitsu user login + password (str): Kitsu user password + kitsu_url (str, optional): Kitsu host URL. Defaults to None. + + Returns: + bool: Are credentials valid? """ + if kitsu_url is None: + kitsu_url = os.environ.get("KITSU_SERVER") + # Connect to server validate_host(kitsu_url) @@ -32,8 +38,11 @@ def validate_credentials( def validate_host(kitsu_url: str) -> bool: """Validate credentials by trying to connect to Kitsu host URL. - :param kitsu_url: Kitsu host URL - :return: Is host valid? + Args: + kitsu_url (str, optional): Kitsu host URL. + + Returns: + bool: Is host valid? """ # Connect to server gazu.set_host(kitsu_url) @@ -58,8 +67,9 @@ def clear_credentials(): def save_credentials(login: str, password: str): """Save credentials in Secure Registry. - :param login: Kitsu Login - :param password: Kitsu Password + Args: + login (str): Kitsu user login + password (str): Kitsu user password """ # Get user registry user_registry = OpenPypeSecureRegistry("kitsu_user") @@ -72,7 +82,8 @@ def save_credentials(login: str, password: str): def load_credentials() -> Tuple[str, str]: """Load registered credentials. - :return: Login, Password + Returns: + Tuple[str, str]: (Login, Password) """ # Get user registry user_registry = OpenPypeSecureRegistry("kitsu_user") @@ -85,8 +96,9 @@ def load_credentials() -> Tuple[str, str]: def set_credentials_envs(login: str, password: str): """Set environment variables with Kitsu login and password. - :param login: Kitsu Login - :param password: Kitsu Password + Args: + login (str): Kitsu user login + password (str): Kitsu user password """ os.environ["KITSU_LOGIN"] = login os.environ["KITSU_PWD"] = password From 5db0c1dfa7cbf483976b6e52f6d4cde5813daedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 4 Mar 2022 10:12:08 +0100 Subject: [PATCH 037/194] Python 2 compat and cleaning --- openpype/modules/kitsu/kitsu_module.py | 4 ++-- openpype/modules/kitsu/utils/sync_service.py | 5 ----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index dca6133e88..53edfddf9a 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -105,7 +105,7 @@ def cli_main(): @click.option( "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -def push_to_zou(login: str, password: str): +def push_to_zou(login, password): """Synchronize Zou database (Kitsu backend) with openpype database. Args: @@ -122,7 +122,7 @@ def push_to_zou(login: str, password: str): @click.option( "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login: str, password: str): +def sync_service(login, password): """Synchronize openpype database from Zou sever database. Args: diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 831673ec0d..6bf98cf308 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -383,8 +383,3 @@ def start_listeners(login: str, password: str): # Connect to server listener = Listener(login, password) listener.start() - - -if __name__ == "__main__": - # TODO not sure when this can be run and if this system is reliable - start_listeners(load_credentials()) From 8c3b510887564d52d7230c7114a17f9044157be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 4 Mar 2022 10:12:58 +0100 Subject: [PATCH 038/194] Cleaning --- 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 6bf98cf308..2e8fbf77f5 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -3,7 +3,7 @@ import os import gazu from avalon.api import AvalonMongoDB -from .credentials import load_credentials, validate_credentials +from .credentials import validate_credentials from .update_op_with_zou import ( create_op_asset, set_op_project, From e5ae5459e135d10d01e1549f8ac3f8a8cb83e689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 4 Mar 2022 15:58:06 +0100 Subject: [PATCH 039/194] Kitsu docs --- website/docs/artist_kitsu.md | 17 ++++++++ .../docs/assets/kitsu/kitsu_credentials.png | Bin 0 -> 15798 bytes website/docs/module_kitsu.md | 37 ++++++++++++++++++ website/sidebars.js | 2 + 4 files changed, 56 insertions(+) create mode 100644 website/docs/artist_kitsu.md create mode 100644 website/docs/assets/kitsu/kitsu_credentials.png create mode 100644 website/docs/module_kitsu.md diff --git a/website/docs/artist_kitsu.md b/website/docs/artist_kitsu.md new file mode 100644 index 0000000000..9ef782c297 --- /dev/null +++ b/website/docs/artist_kitsu.md @@ -0,0 +1,17 @@ +--- +id: artist_kitsu +title: Kitsu +sidebar_label: Kitsu +--- + +# How to use Kitsu in OpenPype + +## Login to Kitsu module in OpenPype +1. Launch OpenPype, the `Kitsu Credentials` window will open automatically, if not, or if you want to log-in with another account, go to systray OpenPype icon and click on `Kitsu Connect`. +2. Enter your credentials and press *Ok*: + + ![kitsu-login](assets/kitsu/kitsu_credentials.png) + +:::tip +In Kitsu, All the publish actions executed by `pyblish` will be attributed to the currently logged-in user. +::: \ No newline at end of file diff --git a/website/docs/assets/kitsu/kitsu_credentials.png b/website/docs/assets/kitsu/kitsu_credentials.png new file mode 100644 index 0000000000000000000000000000000000000000..25c1ad93c4d966a083d1fbdcd15e78a54e659b76 GIT binary patch literal 15798 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 literal 0 HcmV?d00001 diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md new file mode 100644 index 0000000000..ec38cce5e1 --- /dev/null +++ b/website/docs/module_kitsu.md @@ -0,0 +1,37 @@ +--- +id: module_kitsu +title: Kitsu Administration +sidebar_label: Kitsu +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Kitsu is a great open source production tracker and can be used for project management instead of Ftrack. This documentation assumes that you are familiar with Kitsu and it's basic principles. If you're new to Kitsu, we recommend having a thorough look at [Kitsu Official Documentation](https://kitsu.cg-wire.com/). + +## Prepare Kitsu for OpenPype + +### Server URL +If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kitsu settings. And that's all! +This setting is available for all the users of the OpenPype instance. + +## Synchronize +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. +Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. + +```bash +openpype_console module kitsu sync-service -l me@domain.ext -p my_password +``` + +### Events listening +Listening to Kitsu events is the key to automation of many tasks like _project/episode/sequence/shot/asset/task create/update/delete_ and some more. Events listening should run at all times to perform the required processing as it is not possible to catch some of them retrospectively with strong reliability. If such timeout has been encountered, you must relaunch the `sync-service` command to run the synchronization step again. + +### Push to Kitsu +An utility function is provided to help update Kitsu data (a.k.a Zou database) with OpenPype data if the publishing to the production tracker hasn't been possible for some time. Running `push-to-zou` will create the data on behalf of the user. +:::caution +This functionality cannot deal with all cases and is not error proof, some intervention by a human being might be required. +::: + +```bash +openpype_console module kitsu push-to-zou -l me@domain.ext -p my_password +``` diff --git a/website/sidebars.js b/website/sidebars.js index 105afc30eb..eecdcc6e9a 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -28,6 +28,7 @@ module.exports = { "artist_hosts_photoshop", "artist_hosts_tvpaint", "artist_hosts_unreal", + "artist_kitsu", { type: "category", label: "Ftrack", @@ -75,6 +76,7 @@ module.exports = { label: "Modules", items: [ "module_ftrack", + "module_kitsu", "module_site_sync", "module_deadline", "module_muster", From 05158585c778a3bed91c023626bcf5ee3baf29a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Mar 2022 09:22:16 +0100 Subject: [PATCH 040/194] Add pyblish comment to kitsu --- openpype/modules/kitsu/kitsu_module.py | 6 +++--- openpype/modules/kitsu/plugins/publish/kitsu_plugin.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 53edfddf9a..8e7ab6f78c 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -51,11 +51,11 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): set_credentials_envs, ) - username, password = load_credentials() + login, password = load_credentials() # Check credentials, ask them if needed - if validate_credentials(username, password): - set_credentials_envs(username, password) + if validate_credentials(login, password): + set_credentials_envs(login, password) else: self.show_dialog() diff --git a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py index b556f2b91f..5fce123d7e 100644 --- a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py +++ b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py @@ -39,7 +39,9 @@ class IntegrateRig(pyblish.api.InstancePlugin): gazu.task.add_comment( entity_task, entity_task["task_status_id"], - comment=f"Version {instance.data['version']} has been published!", - ) # TODO add comment from pyblish + comment=f"Version {instance.data['version']} has been published!\n" + "\n" # Add written comment in Pyblish + f"{instance.data['versionEntity']['data']['comment']}".strip(), + ) self.log.info("Version published to Kitsu successfully!") From 1591b91c54611e2bfe55902ad31929e72416515f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 8 Mar 2022 09:57:37 +0100 Subject: [PATCH 041/194] Python2 compat --- openpype/modules/kitsu/plugins/publish/kitsu_plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py index 5fce123d7e..5d6c76bc3f 100644 --- a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py +++ b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py @@ -39,9 +39,11 @@ class IntegrateRig(pyblish.api.InstancePlugin): gazu.task.add_comment( entity_task, entity_task["task_status_id"], - comment=f"Version {instance.data['version']} has been published!\n" - "\n" # Add written comment in Pyblish - f"{instance.data['versionEntity']['data']['comment']}".strip(), + comment="Version {} has been published!\n".format( + instance.data["version"] + ) + # Add written comment in Pyblish + + "\n{}".format(instance.data["versionEntity"]["data"]["comment"]), ) self.log.info("Version published to Kitsu successfully!") From 9c0c43a0612c70fa7fa26e71c1f91b7605b3792d Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Tue, 15 Mar 2022 18:01:04 +0100 Subject: [PATCH 042/194] fix first sync crash --- .../modules/kitsu/utils/update_op_with_zou.py | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index eb675ad09e..e76d54d1ad 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -145,14 +145,15 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: project_data = project["data"] or {} # Update data - project_data.update( - { - "code": project["code"], - "fps": project["fps"], - "resolutionWidth": project["resolution"].split("x")[0], - "resolutionHeight": project["resolution"].split("x")[1], - } - ) + if project_data: + project_data.update( + { + "code": project["code"], + "fps": project["fps"], + "resolutionWidth": project["resolution"].split("x")[0], + "resolutionHeight": project["resolution"].split("x")[1], + } + ) return UpdateOne( {"_id": project_doc["_id"]}, @@ -211,10 +212,6 @@ def sync_project_from_kitsu( project = gazu.project.get_project_by_name(project_name) project_code = project_name - # Try to find project document - project_col = dbcon.database[project_code] - project_doc = project_col.find_one({"type": "project"}) - print(f"Synchronizing {project_name}...") # Get all assets from zou @@ -222,15 +219,15 @@ def sync_project_from_kitsu( all_episodes = gazu.shot.all_episodes_for_project(project) all_seqs = gazu.shot.all_sequences_for_project(project) all_shots = gazu.shot.all_shots_for_project(project) - all_entities = [ - e - for e in all_assets + all_episodes + all_seqs + all_shots - if e["data"] and not e["data"].get("is_substitute") - ] + all_entities = all_assets + all_episodes + all_seqs + all_shots # Sync project. Create if doesn't exist bulk_writes.append(write_project_to_op(project, dbcon)) + # Try to find project document + project_col = dbcon.database[project_code] + project_doc = project_col.find_one({"type": "project"}) + # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc From 17dd018aa060f8fbaf043e19e86a9a0e318f14dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 16 Mar 2022 17:01:30 +0100 Subject: [PATCH 043/194] Build project code --- openpype/modules/kitsu/utils/update_op_with_zou.py | 11 ++++++++++- 1 file changed, 10 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 e76d54d1ad..98f263efe1 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -144,11 +144,20 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: # Project data and tasks project_data = project["data"] or {} + # Build project code and update Kitsu + project_code = project.get("code") + if not project_code: + project_code = project["name"].replace(" ", "_").lower() + project["code"] = project_code + + # Update Zou + gazu.project.update_project(project) + # Update data if project_data: project_data.update( { - "code": project["code"], + "code": project_code, "fps": project["fps"], "resolutionWidth": project["resolution"].split("x")[0], "resolutionHeight": project["resolution"].split("x")[1], From 093e801b0b02074e4d5bd7ecd0ff6d791ba6ea92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 16 Mar 2022 17:31:22 +0100 Subject: [PATCH 044/194] Sync project FPS and resolution --- .../modules/kitsu/utils/update_op_with_zou.py | 17 ++++++++--------- 1 file changed, 8 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 98f263efe1..cb2c79b942 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -154,15 +154,14 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: gazu.project.update_project(project) # Update data - if project_data: - project_data.update( - { - "code": project_code, - "fps": project["fps"], - "resolutionWidth": project["resolution"].split("x")[0], - "resolutionHeight": project["resolution"].split("x")[1], - } - ) + project_data.update( + { + "code": project_code, + "fps": project["fps"], + "resolutionWidth": project["resolution"].split("x")[0], + "resolutionHeight": project["resolution"].split("x")[1], + } + ) return UpdateOne( {"_id": project_doc["_id"]}, From bd06809ffaf8ff8ae8bde463a5ad990b486288c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 16 Mar 2022 18:44:26 +0100 Subject: [PATCH 045/194] Fix shot syncs --- openpype/modules/kitsu/utils/update_op_with_zou.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index cb2c79b942..6aea2e3930 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -1,4 +1,5 @@ """Functions to update OpenPype data using Kitsu DB (a.k.a Zou).""" +from copy import deepcopy from typing import Dict, List from pymongo import DeleteOne, UpdateOne @@ -54,9 +55,14 @@ def update_op_assets( for item in entities_list: # Update asset item_doc = asset_doc_ids[item["id"]] - item_data = item_doc["data"].copy() + item_data = deepcopy(item_doc["data"]) + item_data.update(item.get("data") or {}) item_data["zou"] = item + # Asset settings + item_data["frameStart"] = item_data.get("frame_in") + item_data["frameEnd"] = item_data.get("frame_out") + # Tasks tasks_list = [] if item["type"] == "Asset": @@ -103,9 +109,7 @@ def update_op_assets( # Update 'data' different in zou DB updated_data = { - k: item_data[k] - for k in item_data.keys() - if item_doc["data"].get(k) != item_data[k] + k: v for k, v in item_data.items() if item_doc["data"].get(k) != v } if updated_data or not item_doc.get("parent"): assets_with_update.append( From 84a13e4eb6ff99b433ea1d2fddce32d106381e61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 17 Mar 2022 15:52:14 +0100 Subject: [PATCH 046/194] shots and assets custom folders root --- openpype/modules/kitsu/utils/sync_service.py | 8 +- .../modules/kitsu/utils/update_op_with_zou.py | 82 +++++++++++++++++-- .../defaults/project_settings/kitsu.json | 4 + .../projects_schema/schema_project_kitsu.json | 17 ++++ 4 files changed, 99 insertions(+), 12 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 2e8fbf77f5..746cb843e9 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -167,7 +167,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - [asset], zou_ids_and_asset_docs + project_col[asset], zou_ids_and_asset_docs )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) @@ -214,7 +214,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - [episode], zou_ids_and_asset_docs + project_col, [episode], zou_ids_and_asset_docs )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) @@ -262,7 +262,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - [sequence], zou_ids_and_asset_docs + project_col, [sequence], zou_ids_and_asset_docs )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) @@ -310,7 +310,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - [shot], zou_ids_and_asset_docs + project_col, [shot], zou_ids_and_asset_docs )[0] project_col.update_one({"_id": asset_doc_id}, asset_update) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 6aea2e3930..e2ad29bfa0 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -11,6 +11,7 @@ from gazu.task import ( ) from avalon.api import AvalonMongoDB +from openpype.api import get_project_settings from openpype.lib import create_project from openpype.modules.kitsu.utils.credentials import validate_credentials @@ -42,15 +43,23 @@ def set_op_project(dbcon, project_id) -> Collection: def update_op_assets( - entities_list: List[dict], asset_doc_ids: Dict[str, dict] + project_col: Collection, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. - :param entities_list: List of zou entities to update - :param asset_doc_ids: Dicts of [{zou_id: asset_doc}, ...] - :return: List of (doc_id, update_dict) tuples + Args: + project_col (Collection): Mongo project collection to sync + entities_list (List[dict]): List of zou entities to update + asset_doc_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...] + + Returns: + List[Dict[str, dict]]: List of (doc_id, update_dict) tuples """ + project_name = project_col.name + assets_with_update = [] for item in entities_list: # Update asset @@ -65,9 +74,10 @@ def update_op_assets( # Tasks tasks_list = [] - if item["type"] == "Asset": + item_type = item["type"] + if item_type == "Asset": tasks_list = all_tasks_for_asset(item) - elif item["type"] == "Shot": + elif item_type == "Shot": tasks_list = all_tasks_for_shot(item) # TODO frame in and out item_data["tasks"] = { @@ -91,10 +101,39 @@ def update_op_assets( or item.get("source_id") ) # TODO check consistency - # Visual parent for hierarchy + # Substitute Episode and Sequence by Shot + project_module_settings = get_project_settings(project_name)["kitsu"] + substitute_item_type = ( + "shots" + if item_type in ["Episode", "Sequence"] + else f"{item_type.lower()}s" + ) + entity_parent_folders = [ + f + for f in project_module_settings["entities_root"] + .get(substitute_item_type) + .split("/") + if f + ] + + # Root parent folder if exist visual_parent_doc_id = ( asset_doc_ids[parent_zou_id]["_id"] if parent_zou_id else None ) + if visual_parent_doc_id is None: + # Find root folder doc + root_folder_doc = project_col.find_one( + { + "type": "asset", + "name": entity_parent_folders[-1], + "data.root_of": substitute_item_type, + }, + ["_id"], + ) + if root_folder_doc: + visual_parent_doc_id = root_folder_doc["_id"] + + # Visual parent for hierarchy item_data["visualParent"] = visual_parent_doc_id # Add parents for hierarchy @@ -107,6 +146,9 @@ def update_op_assets( parent_entity = parent_doc["data"]["zou"] parent_zou_id = parent_entity["parent_id"] + # Set root folders parents + item_data["parents"] = entity_parent_folders + item_data["parents"] + # Update 'data' different in zou DB updated_data = { k: v for k, v in item_data.items() if item_doc["data"].get(k) != v @@ -248,6 +290,30 @@ def sync_project_from_kitsu( } zou_ids_and_asset_docs[project["id"]] = project_doc + # Create entities root folders + project_module_settings = get_project_settings(project_name)["kitsu"] + for entity_type, root in project_module_settings["entities_root"].items(): + parent_folders = root.split("/") + direct_parent_doc = None + for i, folder in enumerate(parent_folders, 1): + parent_doc = project_col.find_one( + {"type": "asset", "name": folder, "data.root_of": entity_type} + ) + if not parent_doc: + direct_parent_doc = project_col.insert_one( + { + "name": folder, + "type": "asset", + "schema": "openpype:asset-3.0", + "data": { + "root_of": entity_type, + "parents": parent_folders[:i], + "visualParent": direct_parent_doc, + "tasks": {}, + }, + } + ) + # Create to_insert = [] to_insert.extend( @@ -275,7 +341,7 @@ def sync_project_from_kitsu( [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - all_entities, zou_ids_and_asset_docs + project_col, all_entities, zou_ids_and_asset_docs ) ] ) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 435814a9d1..a37146e1d2 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -1,4 +1,8 @@ { + "entities_root": { + "assets": "Assets", + "shots": "Shots" + }, "entities_naming_pattern": { "episode": "E##", "sequence": "SQ##", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index a504959001..8d71d0ecd6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -5,6 +5,23 @@ "collapsible": true, "is_file": true, "children": [ + { + "type": "dict", + "key": "entities_root", + "label": "Entities root folder", + "children": [ + { + "type": "text", + "key": "assets", + "label": "Assets:" + }, + { + "type": "text", + "key": "shots", + "label": "Shots (includes Episodes & Sequences if any):" + } + ] + }, { "type": "dict", "key": "entities_naming_pattern", From 3970229ce5908abfd62c90a88b656f08f232dbfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 17 Mar 2022 16:09:15 +0100 Subject: [PATCH 047/194] Fix fps fallback to project's value --- openpype/modules/kitsu/utils/update_op_with_zou.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index e2ad29bfa0..288efe30da 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -71,6 +71,10 @@ def update_op_assets( # Asset settings item_data["frameStart"] = item_data.get("frame_in") item_data["frameEnd"] = item_data.get("frame_out") + # Sentinel for fps, fallback to project's value when entity fps is deleted + if not item_data.get("fps") and item_doc["data"].get("fps"): + project_doc = project_col.find_one({"type": "project"}) + item_data["fps"] = project_doc["data"]["fps"] # Tasks tasks_list = [] From 78bda28da4ec26cf4b8b8673cabc01316f578678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 17 Mar 2022 16:49:13 +0100 Subject: [PATCH 048/194] frame_in/out fallbacks --- .../modules/kitsu/utils/update_op_with_zou.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 288efe30da..f5c6406722 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -68,10 +68,19 @@ def update_op_assets( item_data.update(item.get("data") or {}) item_data["zou"] = item - # Asset settings - item_data["frameStart"] = item_data.get("frame_in") - item_data["frameEnd"] = item_data.get("frame_out") - # Sentinel for fps, fallback to project's value when entity fps is deleted + # == Asset settings == + # Frame in, fallback on 0 + frame_in = int(item_data.get("frame_in") or 0) + item_data["frameStart"] = frame_in + # Frame out, fallback on frame_in + duration + frames_duration = int(item.get("nb_frames") or 1) + frame_out = ( + item_data["frame_out"] + if item_data.get("frame_out") + else frame_in + frames_duration + ) + item_data["frameEnd"] = int(frame_out) + # Fps, fallback to project's value when entity fps is deleted if not item_data.get("fps") and item_doc["data"].get("fps"): project_doc = project_col.find_one({"type": "project"}) item_data["fps"] = project_doc["data"]["fps"] From 527f4a710dbddd21feac630bdafb7b986a354c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 25 Mar 2022 10:03:22 +0100 Subject: [PATCH 049/194] Sync only open projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément Hector --- 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 f5c6406722..f43223cdf7 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -256,7 +256,7 @@ def sync_all_project(login: str, password: str): # Iterate projects dbcon = AvalonMongoDB() dbcon.install() - all_projects = gazu.project.all_projects() + all_projects = gazu.project.all_open_projects() for project in all_projects: sync_project_from_kitsu(project["name"], dbcon, project) From 63096b4e6b819081bfc3ea962ea82ab8d2ef507e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 4 May 2022 18:26:58 +0200 Subject: [PATCH 050/194] change avalon API --- openpype/modules/kitsu/utils/sync_service.py | 2 +- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 746cb843e9..01596e2667 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -2,7 +2,7 @@ import os import gazu -from avalon.api import AvalonMongoDB +from openpype.pipeline import AvalonMongoDB from .credentials import validate_credentials from .update_op_with_zou import ( create_op_asset, diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index f43223cdf7..25c89800d4 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -10,7 +10,7 @@ from gazu.task import ( all_tasks_for_shot, ) -from avalon.api import AvalonMongoDB +from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.lib import create_project from openpype.modules.kitsu.utils.credentials import validate_credentials From 0359ec6da4e03bf9c7df49060a423c028195d86c Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 7 Apr 2022 15:46:40 +0200 Subject: [PATCH 051/194] create kitsu collector --- .../publish/collect_kitsu_credential.py | 18 ++++++ .../plugins/publish/collect_kitsu_entities.py | 64 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py create mode 100644 openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py new file mode 100644 index 0000000000..bd0af16c8b --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py @@ -0,0 +1,18 @@ +import os + +import gazu + +import pyblish.api + + +class CollectKitsuSession(pyblish.api.ContextPlugin): + """Collect Kitsu session using user credentials""" + + order = pyblish.api.CollectorOrder + label = "Kitsu user session" + + + def process(self, context): + + gazu.client.set_host(os.environ["KITSU_SERVER"]) + gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py new file mode 100644 index 0000000000..e4773f7b2a --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -0,0 +1,64 @@ +import os + +import gazu + +import pyblish.api + + +class CollectKitsuEntities(pyblish.api.ContextPlugin): + """Collect Kitsu entities according to the current context""" + + order = pyblish.api.CollectorOrder + 0.499 + label = "Kitsu entities" + + def process(self, context): + + os.environ["AVALON_PROJECT"], + os.environ["AVALON_ASSET"], + os.environ["AVALON_TASK"], + os.environ["AVALON_APP_NAME"] + + asset_data = context.data["assetEntity"]["data"] + zoo_asset_data = asset_data.get("zou") + if not zoo_asset_data: + raise + + kitsu_project = gazu.project.get_project(zoo_asset_data["project_id"]) + if not kitsu_project: + raise + context.data["kitsu_project"] = kitsu_project + + kitsu_asset = gazu.asset.get_asset(zoo_asset_data["entity_type_id"]) + if not kitsu_asset: + raise + context.data["kitsu_asset"] = kitsu_asset + + # kitsu_task_type = gazu.task.get_task_type_by_name(instance.data["task"]) + # if not kitsu_task_type: + # raise + # context.data["kitsu_task_type"] = kitsu_task_type + + zoo_task_data = asset_data["tasks"][os.environ["AVALON_TASK"]].get("zou") + kitsu_task = gazu.task.get_task( + asset_data["zou"], + kitsu_task_type + ) + if not kitsu_task: + raise + context.data["kitsu_task"] = kitsu_task + + wip = gazu.task.get_task_status_by_short_name("wip") + + task = gazu.task.get_task_by_name(asset, modeling) + comment = gazu.task.add_comment(task, wip, "Change status to work in progress") + + person = gazu.person.get_person_by_desktop_login("john.doe") + + # task_type = gazu.task.get_task_type_by_name(instance.data["task"]) + + # entity_task = gazu.task.get_task_by_entity( + # asset_data["zou"], + # task_type + # ) + + raise \ No newline at end of file From 196c81ab78dffdafcca28c00657d11c799a9d7f4 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 8 Apr 2022 12:03:43 +0200 Subject: [PATCH 052/194] collect all kitsu entities in context --- .../plugins/publish/collect_kitsu_entities.py | 51 ++++++------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index e4773f7b2a..f599fd0c14 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -13,52 +13,31 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): def process(self, context): - os.environ["AVALON_PROJECT"], - os.environ["AVALON_ASSET"], - os.environ["AVALON_TASK"], - os.environ["AVALON_APP_NAME"] - asset_data = context.data["assetEntity"]["data"] zoo_asset_data = asset_data.get("zou") if not zoo_asset_data: - raise + raise AssertionError("Zoo asset data not found in OpenPype!") + self.log.debug("Collected zoo asset data: {}".format(zoo_asset_data)) + + zoo_task_data = asset_data["tasks"][os.environ["AVALON_TASK"]].get("zou") + if not zoo_task_data: + raise AssertionError("Zoo task data not found in OpenPype!") + self.log.debug("Collected zoo task data: {}".format(zoo_task_data)) kitsu_project = gazu.project.get_project(zoo_asset_data["project_id"]) if not kitsu_project: - raise + raise AssertionError("Project not not found in kitsu!") context.data["kitsu_project"] = kitsu_project + self.log.debug("Collect kitsu project: {}".format(kitsu_project)) - kitsu_asset = gazu.asset.get_asset(zoo_asset_data["entity_type_id"]) + kitsu_asset = gazu.asset.get_asset(zoo_asset_data["id"]) if not kitsu_asset: - raise + raise AssertionError("Asset not not found in kitsu!") context.data["kitsu_asset"] = kitsu_asset + self.log.debug("Collect kitsu asset: {}".format(kitsu_asset)) - # kitsu_task_type = gazu.task.get_task_type_by_name(instance.data["task"]) - # if not kitsu_task_type: - # raise - # context.data["kitsu_task_type"] = kitsu_task_type - - zoo_task_data = asset_data["tasks"][os.environ["AVALON_TASK"]].get("zou") - kitsu_task = gazu.task.get_task( - asset_data["zou"], - kitsu_task_type - ) + kitsu_task = gazu.task.get_task(zoo_task_data["id"]) if not kitsu_task: - raise + raise AssertionError("Task not not found in kitsu!") context.data["kitsu_task"] = kitsu_task - - wip = gazu.task.get_task_status_by_short_name("wip") - - task = gazu.task.get_task_by_name(asset, modeling) - comment = gazu.task.add_comment(task, wip, "Change status to work in progress") - - person = gazu.person.get_person_by_desktop_login("john.doe") - - # task_type = gazu.task.get_task_type_by_name(instance.data["task"]) - - # entity_task = gazu.task.get_task_by_entity( - # asset_data["zou"], - # task_type - # ) - - raise \ No newline at end of file + self.log.debug("Collect kitsu task: {}".format(kitsu_task)) From eb288959f50666e0c9a4751d7908decd540de9de Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Sat, 9 Apr 2022 11:13:48 +0200 Subject: [PATCH 053/194] integrate note and status --- .../publish/collect_kitsu_credential.py | 2 +- .../plugins/publish/integrate_kitsu_note.py | 36 +++++++++++++++++++ .../plugins/publish/integrate_kitsu_review.py | 16 +++++++++ .../plugins/publish/validate_kitsu_intent.py | 27 ++++++++++++++ 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py create mode 100644 openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py create mode 100644 openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py index bd0af16c8b..c9d94d128a 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py @@ -10,7 +10,7 @@ class CollectKitsuSession(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder label = "Kitsu user session" - + # families = ["kitsu"] def process(self, context): diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py new file mode 100644 index 0000000000..5601dea586 --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -0,0 +1,36 @@ +import gazu +import pyblish.api + + +class IntegrateKitsuNote(pyblish.api.ContextPlugin): + """Integrate Kitsu Note""" + + order = pyblish.api.IntegratorOrder + label = "Kitsu Note and Status" + # families = ["kitsu"] + optional = True + + def process(self, context): + + publish_comment = context.data.get("comment") + if not publish_comment: + self.log.info("Comment is not set.") + + publish_status = context.data.get("intent", {}).get("value") + if not publish_status: + self.log.info("Status is not set.") + + self.log.debug("Comment is `{}`".format(publish_comment)) + self.log.debug("Status is `{}`".format(publish_status)) + + kitsu_status = context.data.get("kitsu_status") + if not kitsu_status: + self.log.info("The status will not be changed") + kitsu_status = context.data["kitsu_task"].get("task_status") + self.log.debug("Kitsu status: {}".format(kitsu_status)) + + gazu.task.add_comment( + context.data["kitsu_task"], + kitsu_status, + comment = publish_comment + ) \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py new file mode 100644 index 0000000000..1853bf569f --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -0,0 +1,16 @@ +# import gazu +import pyblish.api + + +class IntegrateKitsuVersion(pyblish.api.InstancePlugin): + """Integrate Kitsu Review""" + + order = pyblish.api.IntegratorOrder + label = "Kitsu Review" + # families = ["kitsu"] + + def process(self, instance): + pass + + # gazu.task.upload_preview_file(preview, file_path, normalize_movie=True, client=) + # gazu.task.add_preview(task, comment, preview_file_path, normalize_movie=True, client=) \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py new file mode 100644 index 0000000000..9708ebb0dd --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py @@ -0,0 +1,27 @@ +from typing import Optional +import pyblish.api +import gazu + + +class IntegrateKitsuNote(pyblish.api.ContextPlugin): + """Integrate Kitsu Note""" + + order = pyblish.api.ValidatorOrder + label = "Kitsu Intent/Status" + # families = ["kitsu"] + optional = True + + def process(self, context): + + publish_status = context.data.get("intent", {}).get("value") + if not publish_status: + self.log.info("Status is not set.") + + kitsu_status = gazu.task.get_task_status_by_short_name(publish_status) + if not kitsu_status: + raise AssertionError( + "Status `{}` not not found in kitsu!".format(kitsu_status) + ) + self.log.debug("Collect kitsu status: {}".format(kitsu_status)) + + context.data["kitsu_status"] = kitsu_status \ No newline at end of file From ff6c8a6a54b2146fce11b78ea7dccf048536d1e8 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Sun, 10 Apr 2022 10:47:11 +0200 Subject: [PATCH 054/194] Add kitsu log out --- .../plugins/publish/collect_kitsu_credential.py | 4 ++-- .../plugins/publish/collect_kitsu_entities.py | 2 +- .../plugins/publish/integrate_kitsu_file.py | 1 + .../plugins/publish/integrate_kitsu_note.py | 1 + .../plugins/publish/integrate_kitsu_review.py | 3 ++- .../kitsu/plugins/publish/other_kitsu_log_out.py | 16 ++++++++++++++++ 6 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py create mode 100644 openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py index c9d94d128a..4a27117e03 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py @@ -1,11 +1,11 @@ +# -*- coding: utf-8 -*- import os import gazu - import pyblish.api -class CollectKitsuSession(pyblish.api.ContextPlugin): +class CollectKitsuSession(pyblish.api.ContextPlugin): #rename log in """Collect Kitsu session using user credentials""" order = pyblish.api.CollectorOrder diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index f599fd0c14..c5df20b349 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -1,7 +1,7 @@ +# -*- coding: utf-8 -*- import os import gazu - import pyblish.api diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py new file mode 100644 index 0000000000..7c68785e9d --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 5601dea586..8844581237 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import gazu import pyblish.api diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 1853bf569f..a800ca9b57 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -1,4 +1,5 @@ -# import gazu +# -*- coding: utf-8 -*- +import gazu import pyblish.api diff --git a/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py b/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py new file mode 100644 index 0000000000..ff76d9c4f6 --- /dev/null +++ b/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +""".""" +import gazu +import pyblish.api + + +class KitsuLogOut(pyblish.api.ContextPlugin): + """ + Log out from Kitsu API + """ + + order = pyblish.api.IntegratorOrder + 10 + label = "Kitsu Log Out" + + def process(self, context): + gazu.client.log_out() From ed8c01c2639321126b43d84ad83bd06496cb1bad Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 20 Apr 2022 11:57:33 +0200 Subject: [PATCH 055/194] upload file to kitsu --- .../plugins/publish/integrate_kitsu_note.py | 8 ++++--- .../plugins/publish/integrate_kitsu_review.py | 24 +++++++++++++++---- .../plugins/publish/validate_kitsu_intent.py | 5 ++-- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 8844581237..afe388fd82 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -9,7 +9,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" # families = ["kitsu"] - optional = True + # optional = True def process(self, context): @@ -30,8 +30,10 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): kitsu_status = context.data["kitsu_task"].get("task_status") self.log.debug("Kitsu status: {}".format(kitsu_status)) - gazu.task.add_comment( + kitsu_comment = gazu.task.add_comment( context.data["kitsu_task"], kitsu_status, comment = publish_comment - ) \ No newline at end of file + ) + + context.data["kitsu_comment"] = kitsu_comment \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index a800ca9b57..e69937e9bf 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import os import gazu import pyblish.api @@ -6,12 +7,27 @@ import pyblish.api class IntegrateKitsuVersion(pyblish.api.InstancePlugin): """Integrate Kitsu Review""" - order = pyblish.api.IntegratorOrder + order = pyblish.api.IntegratorOrder + 0.01 label = "Kitsu Review" # families = ["kitsu"] def process(self, instance): - pass - # gazu.task.upload_preview_file(preview, file_path, normalize_movie=True, client=) - # gazu.task.add_preview(task, comment, preview_file_path, normalize_movie=True, client=) \ No newline at end of file + context = instance.context + task = context.data["kitsu_task"] + comment = context.data["kitsu_comment"] + + for representation in instance.data.get("representations", []): + + local_path = representation.get("published_path") + self.log.info("*"*40) + self.log.info(local_path) + self.log.info(representation.get("tags", [])) + + # code = os.path.basename(local_path) + + if representation.get("tags", []): + continue + + # gazu.task.upload_preview_file(preview, file_path, normalize_movie=True) + gazu.task.add_preview(task, comment, local_path, normalize_movie=True) \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py index 9708ebb0dd..0597e8546a 100644 --- a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py +++ b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py @@ -1,10 +1,9 @@ -from typing import Optional import pyblish.api import gazu -class IntegrateKitsuNote(pyblish.api.ContextPlugin): - """Integrate Kitsu Note""" +class ValidateKitsuIntent(pyblish.api.ContextPlugin): + """Validate Kitsu Status""" order = pyblish.api.ValidatorOrder label = "Kitsu Intent/Status" From 2e0f6ce42d631674d910dc3caffc1654b6d781f2 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 20 Apr 2022 17:47:17 +0200 Subject: [PATCH 056/194] use task type if no task data in OP --- .../plugins/publish/collect_kitsu_entities.py | 33 +++++++++++++++---- 1 file changed, 27 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 c5df20b349..c907c22e0f 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -21,7 +21,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): zoo_task_data = asset_data["tasks"][os.environ["AVALON_TASK"]].get("zou") if not zoo_task_data: - raise AssertionError("Zoo task data not found in OpenPype!") + self.log.warning("Zoo task data not found in OpenPype!") self.log.debug("Collected zoo task data: {}".format(zoo_task_data)) kitsu_project = gazu.project.get_project(zoo_asset_data["project_id"]) @@ -36,8 +36,29 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): context.data["kitsu_asset"] = kitsu_asset self.log.debug("Collect kitsu asset: {}".format(kitsu_asset)) - kitsu_task = gazu.task.get_task(zoo_task_data["id"]) - if not kitsu_task: - raise AssertionError("Task not not found in kitsu!") - context.data["kitsu_task"] = kitsu_task - self.log.debug("Collect kitsu task: {}".format(kitsu_task)) + if zoo_task_data: + kitsu_task = gazu.task.get_task(zoo_task_data["id"]) + if not kitsu_task: + raise AssertionError("Task not 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"] + ) + ) + + kitsu_task = gazu.task.get_task_by_name( + kitsu_asset, + kitsu_task_type + ) + if not kitsu_task: + raise AssertionError("Task not 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 f72cb8ba9e6201be23f09515e3e3190408fc30a0 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 20 Apr 2022 17:56:39 +0200 Subject: [PATCH 057/194] fix log out --- openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py b/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py index ff76d9c4f6..d7e1616f8d 100644 --- a/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py +++ b/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py @@ -13,4 +13,4 @@ class KitsuLogOut(pyblish.api.ContextPlugin): label = "Kitsu Log Out" def process(self, context): - gazu.client.log_out() + gazu.log_out() From 3d6a6fcbf065c5ed000aea43c7ce4b5db91cd47a Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 20 Apr 2022 18:10:58 +0200 Subject: [PATCH 058/194] upload review --- .../plugins/publish/integrate_kitsu_review.py | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index e69937e9bf..23ee4a668e 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -1,15 +1,17 @@ # -*- coding: utf-8 -*- import os +from typing import Optional import gazu import pyblish.api -class IntegrateKitsuVersion(pyblish.api.InstancePlugin): +class IntegrateKitsuReview(pyblish.api.InstancePlugin): """Integrate Kitsu Review""" order = pyblish.api.IntegratorOrder + 0.01 label = "Kitsu Review" # families = ["kitsu"] + optional = True def process(self, instance): @@ -20,14 +22,16 @@ class IntegrateKitsuVersion(pyblish.api.InstancePlugin): for representation in instance.data.get("representations", []): local_path = representation.get("published_path") - self.log.info("*"*40) - self.log.info(local_path) - self.log.info(representation.get("tags", [])) - # code = os.path.basename(local_path) - - if representation.get("tags", []): + if 'review' not in representation.get("tags", []): continue - # gazu.task.upload_preview_file(preview, file_path, normalize_movie=True) - gazu.task.add_preview(task, comment, local_path, normalize_movie=True) \ No newline at end of file + self.log.debug("Found review at: {}".format(local_path)) + + gazu.task.add_preview( + task, + comment, + local_path, + normalize_movie=True + ) + self.log.info("Review upload on comment") From db8719b895cf553de6f24ad80f610fff28acfc21 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 20 Apr 2022 18:11:38 +0200 Subject: [PATCH 059/194] remove unused import --- .../modules/kitsu/plugins/publish/integrate_kitsu_review.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 23ee4a668e..59b3bcf53e 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -import os -from typing import Optional import gazu import pyblish.api From 7787056c969b0ca91f2f1eee1d58c6c92de2e4f8 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 20 Apr 2022 18:14:14 +0200 Subject: [PATCH 060/194] Do some cleanup --- .../plugins/publish/collect_kitsu_entities.py | 37 +++++++------- .../plugins/publish/integrate_kitsu_file.py | 1 - .../plugins/publish/integrate_kitsu_note.py | 5 +- .../plugins/publish/integrate_kitsu_review.py | 8 +-- .../kitsu/plugins/publish/kitsu_plugin.py | 49 ------------------- .../plugins/publish/other_kitsu_log_out.py | 1 - .../plugins/publish/validate_kitsu_intent.py | 5 +- 7 files changed, 28 insertions(+), 78 deletions(-) delete mode 100644 openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py delete mode 100644 openpype/modules/kitsu/plugins/publish/kitsu_plugin.py diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index c907c22e0f..935b020641 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -14,35 +14,36 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): def process(self, context): asset_data = context.data["assetEntity"]["data"] - zoo_asset_data = asset_data.get("zou") - if not zoo_asset_data: - raise AssertionError("Zoo asset data not found in OpenPype!") - self.log.debug("Collected zoo asset data: {}".format(zoo_asset_data)) + 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)) - zoo_task_data = asset_data["tasks"][os.environ["AVALON_TASK"]].get("zou") - if not zoo_task_data: - self.log.warning("Zoo task data not found in OpenPype!") - self.log.debug("Collected zoo task data: {}".format(zoo_task_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(zoo_asset_data["project_id"]) + kitsu_project = gazu.project.get_project(zou_asset_data["project_id"]) if not kitsu_project: - raise AssertionError("Project not not found in kitsu!") + raise AssertionError("Project not found in kitsu!") context.data["kitsu_project"] = kitsu_project self.log.debug("Collect kitsu project: {}".format(kitsu_project)) - kitsu_asset = gazu.asset.get_asset(zoo_asset_data["id"]) + kitsu_asset = gazu.asset.get_asset(zou_asset_data["id"]) if not kitsu_asset: - raise AssertionError("Asset not not found in kitsu!") + raise AssertionError("Asset not found in kitsu!") context.data["kitsu_asset"] = kitsu_asset self.log.debug("Collect kitsu asset: {}".format(kitsu_asset)) - if zoo_task_data: - kitsu_task = gazu.task.get_task(zoo_task_data["id"]) + if zou_task_data: + kitsu_task = gazu.task.get_task(zou_task_data["id"]) if not kitsu_task: - raise AssertionError("Task not not found in kitsu!") + 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"] @@ -59,6 +60,6 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): kitsu_task_type ) if not kitsu_task: - raise AssertionError("Task not not found in kitsu!") + 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("Collect kitsu task: {}".format(kitsu_task)) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py deleted file mode 100644 index 7c68785e9d..0000000000 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_file.py +++ /dev/null @@ -1 +0,0 @@ -# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index afe388fd82..61e4d2454c 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -9,7 +9,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" # families = ["kitsu"] - # optional = True def process(self, context): @@ -31,8 +30,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Kitsu status: {}".format(kitsu_status)) kitsu_comment = gazu.task.add_comment( - context.data["kitsu_task"], - kitsu_status, + context.data["kitsu_task"], + kitsu_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 59b3bcf53e..c38f14e8a4 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -19,17 +19,17 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): for representation in instance.data.get("representations", []): - local_path = representation.get("published_path") + review_path = representation.get("published_path") if 'review' not in representation.get("tags", []): continue - - self.log.debug("Found review at: {}".format(local_path)) + + self.log.debug("Found review at: {}".format(review_path)) gazu.task.add_preview( task, comment, - local_path, + review_path, normalize_movie=True ) self.log.info("Review upload on comment") diff --git a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py b/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py deleted file mode 100644 index 5d6c76bc3f..0000000000 --- a/openpype/modules/kitsu/plugins/publish/kitsu_plugin.py +++ /dev/null @@ -1,49 +0,0 @@ -import os - -import gazu - -import pyblish.api - - -class CollectExampleAddon(pyblish.api.ContextPlugin): - order = pyblish.api.CollectorOrder + 0.4 - label = "Collect Kitsu" - - def process(self, context): - self.log.info("I'm in Kitsu's plugin!") - - -class IntegrateRig(pyblish.api.InstancePlugin): - """Copy files to an appropriate location where others may reach it""" - - order = pyblish.api.IntegratorOrder - families = ["model"] - - def process(self, instance): - - # Connect to server - gazu.client.set_host(os.environ["KITSU_SERVER"]) - - # Authenticate - gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) - - asset_data = instance.data["assetEntity"]["data"] - - # Get task - task_type = gazu.task.get_task_type_by_name(instance.data["task"]) - entity_task = gazu.task.get_task_by_entity( - asset_data["zou"], task_type - ) - - # Comment entity - gazu.task.add_comment( - entity_task, - entity_task["task_status_id"], - comment="Version {} has been published!\n".format( - instance.data["version"] - ) - # Add written comment in Pyblish - + "\n{}".format(instance.data["versionEntity"]["data"]["comment"]), - ) - - self.log.info("Version published to Kitsu successfully!") diff --git a/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py b/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py index d7e1616f8d..c4a5b390e0 100644 --- a/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py +++ b/openpype/modules/kitsu/plugins/publish/other_kitsu_log_out.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -""".""" import gazu import pyblish.api diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py index 0597e8546a..c82130b33b 100644 --- a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py +++ b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pyblish.api import gazu @@ -9,7 +10,7 @@ class ValidateKitsuIntent(pyblish.api.ContextPlugin): label = "Kitsu Intent/Status" # families = ["kitsu"] optional = True - + def process(self, context): publish_status = context.data.get("intent", {}).get("value") @@ -19,7 +20,7 @@ class ValidateKitsuIntent(pyblish.api.ContextPlugin): kitsu_status = gazu.task.get_task_status_by_short_name(publish_status) if not kitsu_status: raise AssertionError( - "Status `{}` not not found in kitsu!".format(kitsu_status) + "Status `{}` not found in kitsu!".format(kitsu_status) ) self.log.debug("Collect kitsu status: {}".format(kitsu_status)) From d9062b762ab82b85fb048084ef27ebf863a92366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 5 May 2022 11:49:16 +0200 Subject: [PATCH 061/194] Update openpype/modules/kitsu/utils/update_zou_with_op.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/modules/kitsu/utils/update_zou_with_op.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index d1fcde5601..526159d101 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -6,7 +6,7 @@ from typing import List import gazu from pymongo import UpdateOne -from avalon.api import AvalonMongoDB +from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials From 26fcd19a5d8bed9849c924c62a949b25e62c6189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 11 May 2022 12:29:30 +0200 Subject: [PATCH 062/194] use dbcon.Session instead of collection --- openpype/modules/kitsu/utils/sync_service.py | 83 +++++++++---------- .../modules/kitsu/utils/update_op_with_zou.py | 42 +++++----- .../modules/kitsu/utils/update_zou_with_op.py | 6 +- 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 01596e2667..ad18e1f391 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -119,8 +119,8 @@ class Listener: # Write into DB if update_project: - project_col = self.dbcon.database[project_name] - project_col.bulk_write([update_project]) + self.dbcon = self.dbcon.database[project_name] + self.dbcon.bulk_write([update_project]) def _delete_project(self, data): """Delete project.""" @@ -129,28 +129,27 @@ class Listener: project = gazu.project.get_project(data["project_id"]) # Delete project collection - project_col = self.dbcon.database[project["name"]] - project_col.drop() + # self.dbcon = self.dbcon.database[project["name"]] # == Asset == def _new_asset(self, data): """Create new asset into OP DB.""" # Get project entity - project_col = 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"]) # Insert doc in DB - project_col.insert_one(create_op_asset(asset)) + self.dbcon.insert_one(create_op_asset(asset)) # Update self._update_asset(data) def _update_asset(self, data): """Update asset into OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity @@ -160,23 +159,23 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in self.dbcon.find({"type": "asset"}) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[asset["project_id"]] = project_doc # Update asset_doc_id, asset_update = update_op_assets( - project_col[asset], zou_ids_and_asset_docs + self.dbcon[asset], zou_ids_and_asset_docs )[0] - project_col.update_one({"_id": asset_doc_id}, asset_update) + self.dbcon.update_one({"_id": asset_doc_id}, asset_update) def _delete_asset(self, data): """Delete asset of OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) # Delete - project_col.delete_one( + self.dbcon.delete_one( {"type": "asset", "data.zou.id": data["asset_id"]} ) @@ -184,20 +183,20 @@ class Listener: def _new_episode(self, data): """Create new episode into OP DB.""" # Get project entity - project_col = 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"]) # Insert doc in DB - project_col.insert_one(create_op_asset(episode)) + self.dbcon.insert_one(create_op_asset(episode)) # Update self._update_episode(data) def _update_episode(self, data): """Update episode into OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity @@ -207,24 +206,24 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in self.dbcon.find({"type": "asset"}) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[episode["project_id"]] = project_doc # Update asset_doc_id, asset_update = update_op_assets( - project_col, [episode], zou_ids_and_asset_docs + self.dbcon, [episode], zou_ids_and_asset_docs )[0] - project_col.update_one({"_id": asset_doc_id}, asset_update) + self.dbcon.update_one({"_id": asset_doc_id}, asset_update) def _delete_episode(self, data): """Delete shot of OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) print("delete episode") # TODO check bugfix # Delete - project_col.delete_one( + self.dbcon.delete_one( {"type": "asset", "data.zou.id": data["episode_id"]} ) @@ -232,20 +231,20 @@ class Listener: def _new_sequence(self, data): """Create new sequnce into OP DB.""" # Get project entity - project_col = 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"]) # Insert doc in DB - project_col.insert_one(create_op_asset(sequence)) + self.dbcon.insert_one(create_op_asset(sequence)) # Update self._update_sequence(data) def _update_sequence(self, data): """Update sequence into OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity @@ -255,24 +254,24 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in self.dbcon.find({"type": "asset"}) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[sequence["project_id"]] = project_doc # Update asset_doc_id, asset_update = update_op_assets( - project_col, [sequence], zou_ids_and_asset_docs + self.dbcon, [sequence], zou_ids_and_asset_docs )[0] - project_col.update_one({"_id": asset_doc_id}, asset_update) + self.dbcon.update_one({"_id": asset_doc_id}, asset_update) def _delete_sequence(self, data): """Delete sequence of OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) print("delete sequence") # TODO check bugfix # Delete - project_col.delete_one( + self.dbcon.delete_one( {"type": "asset", "data.zou.id": data["sequence_id"]} ) @@ -280,20 +279,20 @@ class Listener: def _new_shot(self, data): """Create new shot into OP DB.""" # Get project entity - project_col = 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"]) # Insert doc in DB - project_col.insert_one(create_op_asset(shot)) + self.dbcon.insert_one(create_op_asset(shot)) # Update self._update_shot(data) def _update_shot(self, data): """Update shot into OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) project_doc = self.dbcon.find_one({"type": "project"}) # Get gazu entity @@ -303,23 +302,23 @@ class Listener: # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in self.dbcon.find({"type": "asset"}) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[shot["project_id"]] = project_doc # Update asset_doc_id, asset_update = update_op_assets( - project_col, [shot], zou_ids_and_asset_docs + self.dbcon, [shot], zou_ids_and_asset_docs )[0] - project_col.update_one({"_id": asset_doc_id}, asset_update) + self.dbcon.update_one({"_id": asset_doc_id}, asset_update) def _delete_shot(self, data): """Delete shot of OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) # Delete - project_col.delete_one( + self.dbcon.delete_one( {"type": "asset", "data.zou.id": data["shot_id"]} ) @@ -327,13 +326,13 @@ class Listener: def _new_task(self, data): """Create new task into OP DB.""" # Get project entity - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) # Get gazu entity task = gazu.task.get_task(data["task_id"]) # Find asset doc - asset_doc = project_col.find_one( + asset_doc = self.dbcon.find_one( {"type": "asset", "data.zou.id": task["entity"]["id"]} ) @@ -341,7 +340,7 @@ 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} - project_col.update_one( + self.dbcon.update_one( {"_id": asset_doc["_id"]}, {"$set": {"data.tasks": asset_tasks}} ) @@ -352,10 +351,10 @@ class Listener: def _delete_task(self, data): """Delete task of OP DB.""" - project_col = set_op_project(self.dbcon, data["project_id"]) + set_op_project(self.dbcon, data["project_id"]) # Find asset doc - asset_docs = [doc for doc in project_col.find({"type": "asset"})] + asset_docs = [doc for doc in self.dbcon.find({"type": "asset"})] for doc in asset_docs: # Match task for name, task in doc["data"]["tasks"].items(): @@ -365,7 +364,7 @@ class Listener: asset_tasks.pop(name) # Delete task in DB - project_col.update_one( + self.dbcon.update_one( {"_id": 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 25c89800d4..6e82ffbd05 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -29,18 +29,17 @@ def create_op_asset(gazu_entity: dict) -> dict: } -def set_op_project(dbcon, project_id) -> Collection: +def set_op_project(dbcon: AvalonMongoDB, project_id: str): """Set project context. - :param dbcon: Connection to DB. - :param project_id: Project zou ID + Args: + dbcon (AvalonMongoDB): Connection to DB. + project_id (str): Project zou ID """ project = gazu.project.get_project(project_id) project_name = project["name"] dbcon.Session["AVALON_PROJECT"] = project_name - return dbcon.database[project_name] - def update_op_assets( project_col: Collection, @@ -258,28 +257,25 @@ def sync_all_project(login: str, password: str): dbcon.install() all_projects = gazu.project.all_open_projects() for project in all_projects: - sync_project_from_kitsu(project["name"], dbcon, project) + sync_project_from_kitsu(dbcon, project) def sync_project_from_kitsu( - project_name: str, dbcon: AvalonMongoDB, project: dict = None + dbcon: AvalonMongoDB, project: dict ): """Update OP project in DB with Zou data. Args: - project_name (str): Name of project to sync dbcon (AvalonMongoDB): MongoDB connection - project (dict, optional): Project dict got using gazu. - Defaults to None. + project (dict): Project dict got using gazu. """ bulk_writes = [] # Get project from zou if not project: - project = gazu.project.get_project_by_name(project_name) - project_code = project_name + project = gazu.project.get_project_by_name(project["name"]) - print(f"Synchronizing {project_name}...") + print(f"Synchronizing {project['name']}...") # Get all assets from zou all_assets = gazu.asset.all_assets_for_project(project) @@ -292,28 +288,28 @@ def sync_project_from_kitsu( bulk_writes.append(write_project_to_op(project, dbcon)) # Try to find project document - project_col = dbcon.database[project_code] - project_doc = project_col.find_one({"type": "project"}) + dbcon.Session["AVALON_PROJECT"] = project["name"] + project_doc = dbcon.find_one({"type": "project"}) # Query all assets of the local project zou_ids_and_asset_docs = { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in dbcon.find({"type": "asset"}) if asset_doc["data"].get("zou", {}).get("id") } zou_ids_and_asset_docs[project["id"]] = project_doc # Create entities root folders - project_module_settings = get_project_settings(project_name)["kitsu"] + project_module_settings = get_project_settings(project["name"])["kitsu"] for entity_type, root in project_module_settings["entities_root"].items(): parent_folders = root.split("/") direct_parent_doc = None for i, folder in enumerate(parent_folders, 1): - parent_doc = project_col.find_one( + parent_doc = dbcon.find_one( {"type": "asset", "name": folder, "data.root_of": entity_type} ) if not parent_doc: - direct_parent_doc = project_col.insert_one( + direct_parent_doc = dbcon.insert_one( { "name": folder, "type": "asset", @@ -338,13 +334,13 @@ def sync_project_from_kitsu( ) if to_insert: # Insert doc in DB - project_col.insert_many(to_insert) + dbcon.insert_many(to_insert) # Update existing docs zou_ids_and_asset_docs.update( { asset_doc["data"]["zou"]["id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in dbcon.find({"type": "asset"}) if asset_doc["data"].get("zou") } ) @@ -354,7 +350,7 @@ def sync_project_from_kitsu( [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - project_col, all_entities, zou_ids_and_asset_docs + dbcon, all_entities, zou_ids_and_asset_docs ) ] ) @@ -373,4 +369,4 @@ def sync_project_from_kitsu( # Write into DB if bulk_writes: - project_col.bulk_write(bulk_writes) + dbcon.bulk_write(bulk_writes) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 526159d101..81d421206f 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -93,10 +93,10 @@ def sync_zou_from_op_project( # Query all assets of the local project project_module_settings = get_project_settings(project_name)["kitsu"] - project_col = dbcon.database[project_name] + dbcon.Session["AVALON_PROJECT"] = project_name asset_docs = { asset_doc["_id"]: asset_doc - for asset_doc in project_col.find({"type": "asset"}) + for asset_doc in dbcon.find({"type": "asset"}) } # Create new assets @@ -259,4 +259,4 @@ def sync_zou_from_op_project( # Write into DB if bulk_writes: - project_col.bulk_write(bulk_writes) + dbcon.bulk_write(bulk_writes) From 765546e6f96cf8ba480c20e65ad3f19cc8a24267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 11 May 2022 12:31:08 +0200 Subject: [PATCH 063/194] fix unused var --- 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 ad18e1f391..8c7ce730a7 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -126,7 +126,7 @@ class Listener: """Delete project.""" # Get project entity print(data) # TODO check bugfix - project = gazu.project.get_project(data["project_id"]) + # project = gazu.project.get_project(data["project_id"]) # Delete project collection # self.dbcon = self.dbcon.database[project["name"]] From 27d9e9bd3e9a9ef03489290101535aeb79617e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 11 May 2022 14:08:05 +0200 Subject: [PATCH 064/194] Fix delete project --- openpype/modules/kitsu/utils/sync_service.py | 9 +++++---- openpype/modules/kitsu/utils/update_op_with_zou.py | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 8c7ce730a7..8d1ffb199d 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -124,12 +124,13 @@ class Listener: def _delete_project(self, data): """Delete project.""" - # Get project entity - print(data) # TODO check bugfix - # project = gazu.project.get_project(data["project_id"]) + project_doc = self.dbcon.find_one( + {"type": "project", "data.zou_id": data["project_id"]} + ) # Delete project collection - # self.dbcon = self.dbcon.database[project["name"]] + self.dbcon.database[project_doc["name"]].drop() + # == Asset == diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 6e82ffbd05..03a10e76a6 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -218,6 +218,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "fps": project["fps"], "resolutionWidth": project["resolution"].split("x")[0], "resolutionHeight": project["resolution"].split("x")[1], + "zou_id": project["id"], } ) From af77d5a888f371c3bab4c2e38483ecaa7e7c6023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 11 May 2022 16:37:17 +0200 Subject: [PATCH 065/194] fix wrong name entities makes crash: they are skipped --- openpype/modules/kitsu/utils/sync_service.py | 2 +- .../modules/kitsu/utils/update_op_with_zou.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 8d1ffb199d..46d3422727 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -131,7 +131,6 @@ class Listener: # Delete project collection self.dbcon.database[project_doc["name"]].drop() - # == Asset == def _new_asset(self, data): @@ -150,6 +149,7 @@ class Listener: def _update_asset(self, data): """Update asset into OP DB.""" + # TODO check if asset doesn't exist, create it (case where name wasn't valid) set_op_project(self.dbcon, data["project_id"]) project_doc = self.dbcon.find_one({"type": "project"}) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 03a10e76a6..fbc23cf52e 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -1,5 +1,6 @@ """Functions to update OpenPype data using Kitsu DB (a.k.a Zou).""" from copy import deepcopy +import re from typing import Dict, List from pymongo import DeleteOne, UpdateOne @@ -16,6 +17,10 @@ from openpype.lib import create_project from openpype.modules.kitsu.utils.credentials import validate_credentials +# Accepted namin pattern for OP +naming_pattern = re.compile("^[a-zA-Z0-9_.]*$") + + def create_op_asset(gazu_entity: dict) -> dict: """Create OP asset dict from gazu entity. @@ -261,9 +266,7 @@ def sync_all_project(login: str, password: str): sync_project_from_kitsu(dbcon, project) -def sync_project_from_kitsu( - dbcon: AvalonMongoDB, project: dict -): +def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): """Update OP project in DB with Zou data. Args: @@ -283,7 +286,11 @@ def sync_project_from_kitsu( all_episodes = gazu.shot.all_episodes_for_project(project) all_seqs = gazu.shot.all_sequences_for_project(project) all_shots = gazu.shot.all_shots_for_project(project) - all_entities = all_assets + all_episodes + all_seqs + all_shots + all_entities = [ + item + for item in all_assets + all_episodes + all_seqs + all_shots + if naming_pattern.match(item["name"]) + ] # Sync project. Create if doesn't exist bulk_writes.append(write_project_to_op(project, dbcon)) From eb5605a3ade0e4f59b8ffb21a7914c446b75372d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 11 May 2022 17:24:11 +0200 Subject: [PATCH 066/194] fix updated asset skipped because of wrong name --- openpype/modules/kitsu/utils/sync_service.py | 9 ++++--- .../modules/kitsu/utils/update_op_with_zou.py | 24 ++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/openpype/modules/kitsu/utils/sync_service.py b/openpype/modules/kitsu/utils/sync_service.py index 46d3422727..6c003942f8 100644 --- a/openpype/modules/kitsu/utils/sync_service.py +++ b/openpype/modules/kitsu/utils/sync_service.py @@ -149,7 +149,6 @@ class Listener: def _update_asset(self, data): """Update asset into OP DB.""" - # TODO check if asset doesn't exist, create it (case where name wasn't valid) set_op_project(self.dbcon, data["project_id"]) project_doc = self.dbcon.find_one({"type": "project"}) @@ -167,7 +166,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - self.dbcon[asset], zou_ids_and_asset_docs + self.dbcon, project_doc, [asset], zou_ids_and_asset_docs )[0] self.dbcon.update_one({"_id": asset_doc_id}, asset_update) @@ -214,7 +213,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - self.dbcon, [episode], zou_ids_and_asset_docs + self.dbcon, project_doc, [episode], zou_ids_and_asset_docs )[0] self.dbcon.update_one({"_id": asset_doc_id}, asset_update) @@ -262,7 +261,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - self.dbcon, [sequence], zou_ids_and_asset_docs + self.dbcon, project_doc, [sequence], zou_ids_and_asset_docs )[0] self.dbcon.update_one({"_id": asset_doc_id}, asset_update) @@ -310,7 +309,7 @@ class Listener: # Update asset_doc_id, asset_update = update_op_assets( - self.dbcon, [shot], zou_ids_and_asset_docs + self.dbcon, project_doc, [shot], zou_ids_and_asset_docs )[0] self.dbcon.update_one({"_id": asset_doc_id}, asset_update) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index fbc23cf52e..fa0bee8365 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -38,7 +38,7 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): """Set project context. Args: - dbcon (AvalonMongoDB): Connection to DB. + dbcon (AvalonMongoDB): Connection to DB project_id (str): Project zou ID """ project = gazu.project.get_project(project_id) @@ -47,7 +47,8 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - project_col: Collection, + dbcon: AvalonMongoDB, + project_doc: dict, entities_list: List[dict], asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: @@ -55,19 +56,27 @@ def update_op_assets( Set 'data' and 'parent' fields. Args: - project_col (Collection): Mongo project collection to sync + dbcon (AvalonMongoDB): Connection to DB entities_list (List[dict]): List of zou entities to update asset_doc_ids (Dict[str, dict]): Dicts of [{zou_id: asset_doc}, ...] Returns: List[Dict[str, dict]]: List of (doc_id, update_dict) tuples """ - project_name = project_col.name + project_name = project_doc["name"] assets_with_update = [] for item in entities_list: + # Check asset exists + 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) + item_doc = dbcon.find_one( + {"type": "asset", "_id": insert_result.inserted_id} + ) + # Update asset - item_doc = asset_doc_ids[item["id"]] item_data = deepcopy(item_doc["data"]) item_data.update(item.get("data") or {}) item_data["zou"] = item @@ -86,7 +95,6 @@ def update_op_assets( item_data["frameEnd"] = int(frame_out) # Fps, fallback to project's value when entity fps is deleted if not item_data.get("fps") and item_doc["data"].get("fps"): - project_doc = project_col.find_one({"type": "project"}) item_data["fps"] = project_doc["data"]["fps"] # Tasks @@ -139,7 +147,7 @@ def update_op_assets( ) if visual_parent_doc_id is None: # Find root folder doc - root_folder_doc = project_col.find_one( + root_folder_doc = dbcon.find_one( { "type": "asset", "name": entity_parent_folders[-1], @@ -358,7 +366,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, all_entities, zou_ids_and_asset_docs + dbcon, project_doc, all_entities, zou_ids_and_asset_docs ) ] ) From 70bb0fd7bc5c37447e6f7d7d9dcdce5f940de919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 11 May 2022 17:38:46 +0200 Subject: [PATCH 067/194] fix flake --- openpype/modules/kitsu/utils/update_op_with_zou.py | 1 - 1 file changed, 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 fa0bee8365..10349a999c 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -4,7 +4,6 @@ import re from typing import Dict, List from pymongo import DeleteOne, UpdateOne -from pymongo.collection import Collection import gazu from gazu.task import ( all_tasks_for_asset, From 987b5df1ddf44a0feabe27feca8930782a05d18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 09:16:49 +0200 Subject: [PATCH 068/194] optim get_project_settings --- 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 10349a999c..0c72537c94 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,6 +63,7 @@ def update_op_assets( List[Dict[str, dict]]: List of (doc_id, update_dict) tuples """ project_name = project_doc["name"] + project_module_settings = get_project_settings(project_name)["kitsu"] assets_with_update = [] for item in entities_list: @@ -126,7 +127,6 @@ def update_op_assets( ) # TODO check consistency # Substitute Episode and Sequence by Shot - project_module_settings = get_project_settings(project_name)["kitsu"] substitute_item_type = ( "shots" if item_type in ["Episode", "Sequence"] From e0bd8777d1b9563d292a72ff592e461eb47e50f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 09:42:51 +0200 Subject: [PATCH 069/194] pop useless item_data --- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 0c72537c94..673a195747 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -85,6 +85,7 @@ def update_op_assets( # Frame in, fallback on 0 frame_in = int(item_data.get("frame_in") or 0) item_data["frameStart"] = frame_in + item_data.pop("frame_in") # Frame out, fallback on frame_in + duration frames_duration = int(item.get("nb_frames") or 1) frame_out = ( @@ -93,6 +94,7 @@ def update_op_assets( else frame_in + frames_duration ) item_data["frameEnd"] = int(frame_out) + item_data.pop("frame_out") # Fps, fallback to project's value when entity fps is deleted if not item_data.get("fps") and item_doc["data"].get("fps"): item_data["fps"] = project_doc["data"]["fps"] From ae69db29cac488410fa00c547042126831d3be0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 09:59:02 +0200 Subject: [PATCH 070/194] black pyblish --- .../plugins/publish/collect_kitsu_credential.py | 4 ++-- .../plugins/publish/collect_kitsu_entities.py | 16 ++++------------ .../plugins/publish/integrate_kitsu_note.py | 6 ++---- .../plugins/publish/integrate_kitsu_review.py | 9 ++------- .../plugins/publish/validate_kitsu_intent.py | 6 ++---- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py index 4a27117e03..b7f6f67a40 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_credential.py @@ -5,7 +5,7 @@ import gazu import pyblish.api -class CollectKitsuSession(pyblish.api.ContextPlugin): #rename log in +class CollectKitsuSession(pyblish.api.ContextPlugin): # rename log in """Collect Kitsu session using user credentials""" order = pyblish.api.CollectorOrder @@ -15,4 +15,4 @@ class CollectKitsuSession(pyblish.api.ContextPlugin): #rename log in def process(self, context): gazu.client.set_host(os.environ["KITSU_SERVER"]) - gazu.log_in(os.environ["KITSU_LOGIN"], os.environ["KITSU_PWD"]) \ No newline at end of file + 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 935b020641..66c35e54c4 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -19,8 +19,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): 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") + 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)) @@ -45,20 +44,13 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): self.log.debug("Collect kitsu task: {}".format(kitsu_task)) else: - kitsu_task_type = gazu.task.get_task_type_by_name( - os.environ["AVALON_TASK"] - ) + 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"] - ) + "Task type {} not found in Kitsu!".format(os.environ["AVALON_TASK"]) ) - kitsu_task = gazu.task.get_task_by_name( - kitsu_asset, - kitsu_task_type - ) + kitsu_task = gazu.task.get_task_by_name(kitsu_asset, kitsu_task_type) if not kitsu_task: raise AssertionError("Task not found in kitsu!") context.data["kitsu_task"] = kitsu_task diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 61e4d2454c..99d891d514 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -30,9 +30,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Kitsu status: {}".format(kitsu_status)) kitsu_comment = gazu.task.add_comment( - context.data["kitsu_task"], - kitsu_status, - comment = publish_comment + context.data["kitsu_task"], kitsu_status, comment=publish_comment ) - context.data["kitsu_comment"] = kitsu_comment \ No newline at end of file + context.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 c38f14e8a4..65179bc0bf 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -21,15 +21,10 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): review_path = representation.get("published_path") - if 'review' not in representation.get("tags", []): + if "review" not in representation.get("tags", []): continue self.log.debug("Found review at: {}".format(review_path)) - gazu.task.add_preview( - task, - comment, - review_path, - normalize_movie=True - ) + gazu.task.add_preview(task, comment, review_path, normalize_movie=True) self.log.info("Review upload on comment") diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py index c82130b33b..e0fad3b79f 100644 --- a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py +++ b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py @@ -19,9 +19,7 @@ class ValidateKitsuIntent(pyblish.api.ContextPlugin): kitsu_status = gazu.task.get_task_status_by_short_name(publish_status) if not kitsu_status: - raise AssertionError( - "Status `{}` not found in kitsu!".format(kitsu_status) - ) + raise AssertionError("Status `{}` not found in kitsu!".format(kitsu_status)) self.log.debug("Collect kitsu status: {}".format(kitsu_status)) - context.data["kitsu_status"] = kitsu_status \ No newline at end of file + context.data["kitsu_status"] = kitsu_status From 3583d85cd2a7d25abf184411c23a273b0f8671c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 09:59:35 +0200 Subject: [PATCH 071/194] black pyblish --- .../plugins/publish/collect_kitsu_entities.py | 16 ++++++++++++---- .../plugins/publish/integrate_kitsu_review.py | 4 +++- .../plugins/publish/validate_kitsu_intent.py | 4 +++- 3 files changed, 18 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 66c35e54c4..84c400bde9 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -19,7 +19,9 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): 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") + 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)) @@ -44,13 +46,19 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): self.log.debug("Collect kitsu task: {}".format(kitsu_task)) else: - kitsu_task_type = gazu.task.get_task_type_by_name(os.environ["AVALON_TASK"]) + 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"]) + "Task type {} not found in Kitsu!".format( + os.environ["AVALON_TASK"] + ) ) - kitsu_task = gazu.task.get_task_by_name(kitsu_asset, kitsu_task_type) + kitsu_task = gazu.task.get_task_by_name( + kitsu_asset, kitsu_task_type + ) if not kitsu_task: raise AssertionError("Task not found in kitsu!") context.data["kitsu_task"] = kitsu_task diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 65179bc0bf..08fa4ee010 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -26,5 +26,7 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): self.log.debug("Found review at: {}".format(review_path)) - gazu.task.add_preview(task, comment, review_path, normalize_movie=True) + gazu.task.add_preview( + task, comment, review_path, normalize_movie=True + ) self.log.info("Review upload on comment") diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py index e0fad3b79f..6b2635bf05 100644 --- a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py +++ b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py @@ -19,7 +19,9 @@ class ValidateKitsuIntent(pyblish.api.ContextPlugin): kitsu_status = gazu.task.get_task_status_by_short_name(publish_status) if not kitsu_status: - raise AssertionError("Status `{}` not found in kitsu!".format(kitsu_status)) + raise AssertionError( + "Status `{}` not found in kitsu!".format(kitsu_status) + ) self.log.debug("Collect kitsu status: {}".format(kitsu_status)) context.data["kitsu_status"] = kitsu_status From 687f7260ceef7c0c3b2966f387e0f678a0fa0f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 10:57:18 +0200 Subject: [PATCH 072/194] optim publish status intent --- .../modules/kitsu/plugins/publish/validate_kitsu_intent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py index 6b2635bf05..e2023b171e 100644 --- a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py +++ b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py @@ -12,10 +12,11 @@ class ValidateKitsuIntent(pyblish.api.ContextPlugin): optional = True def process(self, context): - + # Check publish status exists publish_status = context.data.get("intent", {}).get("value") if not publish_status: self.log.info("Status is not set.") + return kitsu_status = gazu.task.get_task_status_by_short_name(publish_status) if not kitsu_status: From 20f819c1dd2eab2e6401a80c3996c3a93ca0a089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 12:36:47 +0200 Subject: [PATCH 073/194] change intent wrongly used as status choice --- .../plugins/publish/collect_kitsu_entities.py | 16 +++-------- .../plugins/publish/integrate_kitsu_note.py | 20 ++++++++----- .../plugins/publish/integrate_kitsu_review.py | 21 ++++++++------ .../plugins/publish/validate_kitsu_intent.py | 28 ------------------- 4 files changed, 30 insertions(+), 55 deletions(-) delete mode 100644 openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py diff --git a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py index 84c400bde9..66c35e54c4 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -19,9 +19,7 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): 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" - ) + 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)) @@ -46,19 +44,13 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): self.log.debug("Collect kitsu task: {}".format(kitsu_task)) else: - kitsu_task_type = gazu.task.get_task_type_by_name( - os.environ["AVALON_TASK"] - ) + 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"] - ) + "Task type {} not found in Kitsu!".format(os.environ["AVALON_TASK"]) ) - kitsu_task = gazu.task.get_task_by_name( - kitsu_asset, kitsu_task_type - ) + kitsu_task = gazu.task.get_task_by_name(kitsu_asset, kitsu_task_type) if not kitsu_task: raise AssertionError("Task not found in kitsu!") context.data["kitsu_task"] = kitsu_task diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 99d891d514..980589365d 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -11,24 +11,30 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # families = ["kitsu"] def process(self, context): + # Check if work version for user + is_work_version = bool(context.data.get("intent", {}).get("value")) + if is_work_version: + self.log.info("Work version, nothing pushed to Kitsu.") + return + # Get comment text body publish_comment = context.data.get("comment") if not publish_comment: self.log.info("Comment is not set.") - publish_status = context.data.get("intent", {}).get("value") - if not publish_status: - self.log.info("Status is not set.") - self.log.debug("Comment is `{}`".format(publish_comment)) - self.log.debug("Status is `{}`".format(publish_status)) - kitsu_status = context.data.get("kitsu_status") + # Get Waiting for Approval status + kitsu_status = gazu.task.get_task_status_by_short_name("wfa") if not kitsu_status: - self.log.info("The status will not be changed") + self.log.info( + "Cannot find 'Waiting For Approval' status." + "The status will not be changed" + ) kitsu_status = context.data["kitsu_task"].get("task_status") self.log.debug("Kitsu status: {}".format(kitsu_status)) + # Add comment to kitsu task kitsu_comment = gazu.task.add_comment( context.data["kitsu_task"], kitsu_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 08fa4ee010..76cfe62988 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -15,18 +15,23 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): context = instance.context task = context.data["kitsu_task"] - comment = context.data["kitsu_comment"] + comment = context.data.get("kitsu_comment") - for representation in instance.data.get("representations", []): + # Check comment has been created + if not comment: + self.log.debug("Comment not created, review not pushed to preview.") + return + + # Add review representations as preview of comment + for representation in [ + r + for r in instance.data.get("representations", []) + if "review" in representation.get("tags", []) + ]: review_path = representation.get("published_path") - if "review" not in representation.get("tags", []): - continue - self.log.debug("Found review at: {}".format(review_path)) - gazu.task.add_preview( - task, comment, review_path, normalize_movie=True - ) + gazu.task.add_preview(task, comment, review_path, normalize_movie=True) self.log.info("Review upload on comment") diff --git a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py b/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py deleted file mode 100644 index e2023b171e..0000000000 --- a/openpype/modules/kitsu/plugins/publish/validate_kitsu_intent.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -import pyblish.api -import gazu - - -class ValidateKitsuIntent(pyblish.api.ContextPlugin): - """Validate Kitsu Status""" - - order = pyblish.api.ValidatorOrder - label = "Kitsu Intent/Status" - # families = ["kitsu"] - optional = True - - def process(self, context): - # Check publish status exists - publish_status = context.data.get("intent", {}).get("value") - if not publish_status: - self.log.info("Status is not set.") - return - - kitsu_status = gazu.task.get_task_status_by_short_name(publish_status) - if not kitsu_status: - raise AssertionError( - "Status `{}` not found in kitsu!".format(kitsu_status) - ) - self.log.debug("Collect kitsu status: {}".format(kitsu_status)) - - context.data["kitsu_status"] = kitsu_status From 330d4340cc082307bd2a5bda951d7f2e491279f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 12 May 2022 12:39:03 +0200 Subject: [PATCH 074/194] black --- .../plugins/publish/collect_kitsu_entities.py | 16 ++++++++++++---- .../plugins/publish/integrate_kitsu_review.py | 10 +++++++--- 2 files changed, 19 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 66c35e54c4..84c400bde9 100644 --- a/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py +++ b/openpype/modules/kitsu/plugins/publish/collect_kitsu_entities.py @@ -19,7 +19,9 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): 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") + 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)) @@ -44,13 +46,19 @@ class CollectKitsuEntities(pyblish.api.ContextPlugin): self.log.debug("Collect kitsu task: {}".format(kitsu_task)) else: - kitsu_task_type = gazu.task.get_task_type_by_name(os.environ["AVALON_TASK"]) + 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"]) + "Task type {} not found in Kitsu!".format( + os.environ["AVALON_TASK"] + ) ) - kitsu_task = gazu.task.get_task_by_name(kitsu_asset, kitsu_task_type) + kitsu_task = gazu.task.get_task_by_name( + kitsu_asset, kitsu_task_type + ) if not kitsu_task: raise AssertionError("Task not found in kitsu!") context.data["kitsu_task"] = kitsu_task diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 76cfe62988..57e0286b00 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -19,19 +19,23 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): # Check comment has been created if not comment: - self.log.debug("Comment not created, review not pushed to preview.") + self.log.debug( + "Comment not created, review not pushed to preview." + ) return # Add review representations as preview of comment for representation in [ r for r in instance.data.get("representations", []) - if "review" in representation.get("tags", []) + if "review" in r.get("tags", []) ]: review_path = representation.get("published_path") self.log.debug("Found review at: {}".format(review_path)) - gazu.task.add_preview(task, comment, review_path, normalize_movie=True) + gazu.task.add_preview( + task, comment, review_path, normalize_movie=True + ) self.log.info("Review upload on comment") From 631ccf6318916d9c7ad98830dba3007c073e70db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Mon, 16 May 2022 10:24:39 +0200 Subject: [PATCH 075/194] Update openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../kitsu/plugins/publish/integrate_kitsu_review.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py index 57e0286b00..a036f5f9cc 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -25,12 +25,9 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): return # Add review representations as preview of comment - for representation in [ - r - for r in instance.data.get("representations", []) - if "review" in r.get("tags", []) - ]: - + for representation in instance.data.get("representations", []): + if "review" not in r.get("tags", []): + continue review_path = representation.get("published_path") self.log.debug("Found review at: {}".format(review_path)) From 15aa5709ae18c3cca83e73fef9eaa557684a0a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Mon, 16 May 2022 10:25:41 +0200 Subject: [PATCH 076/194] cleaning --- .../modules/kitsu/plugins/publish/integrate_kitsu_review.py | 4 +++- 1 file changed, 3 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 a036f5f9cc..bf80095225 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_review.py @@ -26,8 +26,10 @@ class IntegrateKitsuReview(pyblish.api.InstancePlugin): # Add review representations as preview of comment for representation in instance.data.get("representations", []): - if "review" not in r.get("tags", []): + # Skip if not tagged as review + if "review" not in representation.get("tags", []): continue + review_path = representation.get("published_path") self.log.debug("Found review at: {}".format(review_path)) From e6286166fba934945756357ee08b5d6d89f95cde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Mon, 16 May 2022 11:33:35 +0200 Subject: [PATCH 077/194] remove default intent effect on review integration --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 980589365d..9e067a8ecb 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -11,11 +11,6 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): # families = ["kitsu"] def process(self, context): - # Check if work version for user - is_work_version = bool(context.data.get("intent", {}).get("value")) - if is_work_version: - self.log.info("Work version, nothing pushed to Kitsu.") - return # Get comment text body publish_comment = context.data.get("comment") From 05cb2e4bd938ee1c533bb39625f4d3bbb7b2dad4 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 16 May 2022 13:49:39 +0200 Subject: [PATCH 078/194] waiting approval status can be set in project settings --- .../plugins/publish/integrate_kitsu_note.py | 9 ++++--- .../defaults/project_settings/kitsu.json | 5 ++++ .../projects_schema/schema_project_kitsu.json | 25 +++++++++++++++++++ 3 files changed, 36 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 9e067a8ecb..876eb6bf29 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -9,6 +9,7 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" # families = ["kitsu"] + waiting_for_approval_status = "wfa" def process(self, context): @@ -20,11 +21,13 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Comment is `{}`".format(publish_comment)) # Get Waiting for Approval status - kitsu_status = gazu.task.get_task_status_by_short_name("wfa") + kitsu_status = gazu.task.get_task_status_by_short_name( + self.waiting_for_approval_status + ) if not kitsu_status: self.log.info( - "Cannot find 'Waiting For Approval' status." - "The status will not be changed" + "Cannot find {} status. The status will not be " + "changed!".format(self.waiting_for_approval_status) ) kitsu_status = context.data["kitsu_task"].get("task_status") self.log.debug("Kitsu status: {}".format(kitsu_status)) diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index a37146e1d2..2f1566d89a 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -7,5 +7,10 @@ "episode": "E##", "sequence": "SQ##", "shot": "SH##" + }, + "publish": { + "IntegrateKitsuNote": { + "waiting_for_approval_status": "wfa" + } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index 8d71d0ecd6..cffd7ff578 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -43,6 +43,31 @@ "label": "Shot:" } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "label", + "label": "Integrator" + }, + { + "type": "dict", + "collapsible": true, + "key": "IntegrateKitsuNote", + "label": "Integrate Kitsu Note", + "children": [ + { + "type": "text", + "key": "waiting_for_approval_status", + "label": "Waiting for Aproval Status:" + } + ] + } + ] } ] } From 0b0a9ca2815251ba423f543e376a38e5805c0aba Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 16 May 2022 15:46:47 +0200 Subject: [PATCH 079/194] by default use task status if not specified in config --- .../plugins/publish/integrate_kitsu_note.py | 34 ++++++++++++------- .../defaults/project_settings/kitsu.json | 3 +- .../projects_schema/schema_project_kitsu.json | 9 +++-- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 876eb6bf29..ae559e660e 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from distutils.log import debug import gazu import pyblish.api @@ -9,7 +10,8 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder label = "Kitsu Note and Status" # families = ["kitsu"] - waiting_for_approval_status = "wfa" + set_status_note = False + note_status_shortname = "wfa" def process(self, context): @@ -20,21 +22,29 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): self.log.debug("Comment is `{}`".format(publish_comment)) - # Get Waiting for Approval status - kitsu_status = gazu.task.get_task_status_by_short_name( - self.waiting_for_approval_status - ) - if not kitsu_status: - self.log.info( - "Cannot find {} status. The status will not be " - "changed!".format(self.waiting_for_approval_status) + # 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 ) - kitsu_status = context.data["kitsu_task"].get("task_status") - self.log.debug("Kitsu status: {}".format(kitsu_status)) + if not kitsu_status: + self.log.info( + "Cannot find {} status. The status will not be " + "changed!".format(self.note_status_shortname) + ) + else: + note_status = kitsu_status + self.log.info("Note Kitsu status: {}".format(note_status)) # Add comment to kitsu task + self.log.debug("Add new note in taks id {}".format( + context.data["kitsu_task"]['id'])) kitsu_comment = gazu.task.add_comment( - context.data["kitsu_task"], kitsu_status, comment=publish_comment + context.data["kitsu_task"], + note_status, + comment=publish_comment ) context.data["kitsu_comment"] = kitsu_comment diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 2f1566d89a..ba02d8d259 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -10,7 +10,8 @@ }, "publish": { "IntegrateKitsuNote": { - "waiting_for_approval_status": "wfa" + "set_status_note": false, + "note_status_shortname": "wfa" } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json index cffd7ff578..014a1b7886 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_kitsu.json @@ -60,10 +60,15 @@ "key": "IntegrateKitsuNote", "label": "Integrate Kitsu Note", "children": [ + { + "type": "boolean", + "key": "set_status_note", + "label": "Set status on note" + }, { "type": "text", - "key": "waiting_for_approval_status", - "label": "Waiting for Aproval Status:" + "key": "note_status_shortname", + "label": "Note shortname" } ] } From 5059c0cedff88d33f7c0044f0020fa03d1cdca48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Hector?= Date: Tue, 17 May 2022 11:43:28 +0200 Subject: [PATCH 080/194] Update openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Félix David --- .../modules/kitsu/plugins/publish/integrate_kitsu_note.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index ae559e660e..78c5170856 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -29,14 +29,14 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): kitsu_status = gazu.task.get_task_status_by_short_name( self.note_status_shortname ) - if not kitsu_status: + 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) ) - else: - note_status = kitsu_status - self.log.info("Note Kitsu status: {}".format(note_status)) # Add comment to kitsu task self.log.debug("Add new note in taks id {}".format( From 667cff319d70ea699ded5d009872588f5d5ffee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 17 May 2022 11:49:54 +0200 Subject: [PATCH 081/194] black --- .../kitsu/plugins/publish/integrate_kitsu_note.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 78c5170856..3cd1f450ca 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -22,7 +22,7 @@ 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 + # 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: @@ -39,12 +39,13 @@ class IntegrateKitsuNote(pyblish.api.ContextPlugin): ) # Add comment to kitsu task - self.log.debug("Add new note in taks id {}".format( - context.data["kitsu_task"]['id'])) + self.log.debug( + "Add new note in taks id {}".format( + context.data["kitsu_task"]["id"] + ) + ) kitsu_comment = gazu.task.add_comment( - context.data["kitsu_task"], - note_status, - comment=publish_comment + context.data["kitsu_task"], note_status, comment=publish_comment ) context.data["kitsu_comment"] = kitsu_comment From 6976546505590a59072095f675778c9e8a71fe03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Tue, 17 May 2022 11:51:58 +0200 Subject: [PATCH 082/194] cleaning --- openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py index 3cd1f450ca..ea98e0b7cc 100644 --- a/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py +++ b/openpype/modules/kitsu/plugins/publish/integrate_kitsu_note.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -from distutils.log import debug import gazu import pyblish.api From 13b4b18d162ed4307408e9d2f64869403c740724 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 19 May 2022 17:05:55 +0200 Subject: [PATCH 083/194] OP-2787 - WIP implementation --- openpype/api.py | 2 + openpype/hosts/maya/api/pipeline.py | 7 + .../maya/plugins/create/create_animation.py | 4 + .../maya/plugins/create/create_pointcache.py | 4 + .../maya/plugins/publish/collect_animation.py | 3 + .../plugins/publish/collect_pointcache.py | 14 ++ .../maya/plugins/publish/extract_animation.py | 8 + .../plugins/publish/extract_pointcache.py | 8 + openpype/lib/remote_publish.py | 2 +- .../submit_maya_remote_publish_deadline.py | 137 ++++++++++++++++++ openpype/plugin.py | 12 ++ openpype/plugins/publish/integrate_new.py | 3 + openpype/scripts/remote_publish.py | 11 ++ 13 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_pointcache.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py create mode 100644 openpype/scripts/remote_publish.py diff --git a/openpype/api.py b/openpype/api.py index 9ce745b653..e049a683c6 100644 --- a/openpype/api.py +++ b/openpype/api.py @@ -44,6 +44,7 @@ from . import resources from .plugin import ( Extractor, + Integrator, ValidatePipelineOrder, ValidateContentsOrder, @@ -86,6 +87,7 @@ __all__ = [ # plugin classes "Extractor", + "Integrator", # ordering "ValidatePipelineOrder", "ValidateContentsOrder", diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index b0e8fac635..b75af29523 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -71,8 +71,15 @@ def install(): if lib.IS_HEADLESS: log.info(("Running in headless mode, skipping Maya " "save/open/new callback installation..")) + + # Register default "local" target + print("Registering pyblish target: farm") + pyblish.api.register_target("farm") return + print("Registering pyblish target: local") + pyblish.api.register_target("local") + _set_project() _register_callbacks() diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index 11a668cfc8..5cd1f7090a 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -38,3 +38,7 @@ class CreateAnimation(plugin.Creator): # Default to exporting world-space self.data["worldSpace"] = True + + # Default to not send to farm. + self.data["farm"] = False + self.data["priority"] = 50 diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index ede152f1ef..e876015adb 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -28,3 +28,7 @@ class CreatePointCache(plugin.Creator): # Add options for custom attributes self.data["attr"] = "" self.data["attrPrefix"] = "" + + # Default to not send to farm. + self.data["farm"] = False + self.data["priority"] = 50 diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index 9b1e38fd0a..b442113fbc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -55,3 +55,6 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): # Store data in the instance for the validator instance.data["out_hierarchy"] = hierarchy + + if instance.data.get("farm"): + instance.data["families"].append("deadline") diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py new file mode 100644 index 0000000000..b55babe372 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -0,0 +1,14 @@ +import pyblish.api + + +class CollectPointcache(pyblish.api.InstancePlugin): + """Collect pointcache data for instance.""" + + order = pyblish.api.CollectorOrder + 0.4 + families = ["pointcache"] + label = "Collect Pointcache" + hosts = ["maya"] + + def process(self, instance): + if instance.data.get("farm"): + instance.data["families"].append("deadline") \ No newline at end of file diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index 8a8bd67cd8..87f2d35192 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -23,6 +23,14 @@ class ExtractAnimation(openpype.api.Extractor): families = ["animation"] def process(self, instance): + if instance.data.get("farm"): + path = os.path.join( + os.path.dirname(instance.context.data["currentFile"]), + "cache", + instance.data["name"] + ".abc" + ) + instance.data["expectedFiles"] = [os.path.normpath(path)] + return # Collect the out set nodes out_sets = [node for node in instance if node.endswith("out_SET")] diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 60502fdde1..7ad4c6dfa9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -25,6 +25,14 @@ class ExtractAlembic(openpype.api.Extractor): "vrayproxy"] def process(self, instance): + if instance.data.get("farm"): + path = os.path.join( + os.path.dirname(instance.context.data["currentFile"]), + "cache", + instance.data["name"] + ".abc" + ) + instance.data["expectedFiles"] = [os.path.normpath(path)] + return nodes = instance[:] diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 8a42daf4e9..da2497e1a5 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -228,7 +228,7 @@ def _get_close_plugin(close_plugin_name, log): if plugin.__name__ == close_plugin_name: return plugin - log.warning("Close plugin not found, app might not close.") + log.debug("Close plugin not found, app might not close.") def get_task_data(batch_dir): diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py new file mode 100644 index 0000000000..761bc8cc95 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -0,0 +1,137 @@ +import os +import requests + +from maya import cmds + +from openpype.pipeline import legacy_io + +import pyblish.api + + +class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): + """Submit Maya scene to perform a local publish in Deadline. + + Publishing in Deadline can be helpful for scenes that publish very slow. + This way it can process in the background on another machine without the + Artist having to wait for the publish to finish on their local machine. + + Submission is done through the Deadline Web Service. + + Different from `ProcessSubmittedJobOnFarm` which creates publish job + depending on metadata json containing context and instance data of + rendered files. + """ + + label = "Submit Scene to Deadline" + order = pyblish.api.IntegratorOrder + hosts = ["maya"] + families = ["deadline"] + + # custom deadline attributes + deadline_department = "" + deadline_pool = "" + deadline_pool_secondary = "" + deadline_group = "" + deadline_chunk_size = 1 + deadline_priority = 50 + + def process(self, context): + + # Ensure no errors so far + assert all(result["success"] for result in context.data["results"]), ( + "Errors found, aborting integration..") + + # Note that `publish` data member might change in the future. + # See: https://github.com/pyblish/pyblish-base/issues/307 + actives = [i for i in context if i.data["publish"]] + instance_names = sorted(instance.name for instance in actives) + + if not instance_names: + self.log.warning("No active instances found. " + "Skipping submission..") + return + + scene = context.data["currentFile"] + scenename = os.path.basename(scene) + + # Get project code + project_name = legacy_io.Session["AVALON_PROJECT"] + + job_name = "{scene} [PUBLISH]".format(scene=scenename) + batch_name = "{code} - {scene}".format(code=project_name, + scene=scenename) + + # Generate the payload for Deadline submission + payload = { + "JobInfo": { + "Plugin": "MayaBatch", + "BatchName": batch_name, + "Priority": 50, + "Name": job_name, + "UserName": context.data["user"], + # "Comment": instance.context.data.get("comment", ""), + # "InitialStatus": state + "Department": self.deadline_department, + "ChunkSize": self.deadline_chunk_size, + "Priority": self.deadline_priority, + + "Group": self.deadline_group, + + }, + "PluginInfo": { + + "Build": None, # Don't force build + "StrictErrorChecking": True, + "ScriptJob": True, + + # Inputs + "SceneFile": scene, + "ScriptFilename": "{OPENPYPE_ROOT}/scripts/remote_publish.py", + + # Mandatory for Deadline + "Version": cmds.about(version=True), + + # Resolve relative references + "ProjectPath": cmds.workspace(query=True, + rootDirectory=True), + + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + + # Include critical environment variables with submission + api.Session + keys = [ + "FTRACK_API_USER", + "FTRACK_API_KEY", + "FTRACK_SERVER" + ] + environment = dict({key: os.environ[key] for key in keys + if key in os.environ}, **legacy_io.Session) + + # TODO replace legacy_io with context.data ? + environment["AVALON_PROJECT"] = legacy_io.Session["AVALON_PROJECT"] + environment["AVALON_ASSET"] = legacy_io.Session["AVALON_ASSET"] + environment["AVALON_TASK"] = legacy_io.Session["AVALON_TASK"] + environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME") + environment["OPENPYPE_LOG_NO_COLORS"] = "1" + environment["OPENPYPE_USERNAME"] = context.data["user"] + environment["OPENPYPE_PUBLISH_JOB"] = "1" + environment["OPENPYPE_RENDER_JOB"] = "0" + environment["PYBLISH_ACTIVE_INSTANCES"] = ",".join(instance_names) + + payload["JobInfo"].update({ + "EnvironmentKeyValue%d" % index: "{key}={value}".format( + key=key, + value=environment[key] + ) for index, key in enumerate(environment) + }) + + self.log.info("Submitting Deadline job ...") + deadline_url = context.data["defaultDeadline"] + assert deadline_url, "Requires Deadline Webservice URL" + url = "{}/api/jobs".format(deadline_url) + response = requests.post(url, json=payload, timeout=10) + if not response.ok: + raise Exception(response.text) diff --git a/openpype/plugin.py b/openpype/plugin.py index bb9bc2ff85..f1ee626ffb 100644 --- a/openpype/plugin.py +++ b/openpype/plugin.py @@ -18,6 +18,16 @@ class InstancePlugin(pyblish.api.InstancePlugin): super(InstancePlugin, cls).process(cls, *args, **kwargs) +class Integrator(InstancePlugin): + """Integrator base class. + + Wraps pyblish instance plugin. Targets set to "local" which means all + integrators should run on "local" publishes, by default. + "farm" targets could be used for integrators that should run on a farm. + """ + targets = ["local"] + + class Extractor(InstancePlugin): """Extractor base class. @@ -28,6 +38,8 @@ class Extractor(InstancePlugin): """ + targets = ["local"] + order = 2.0 def staging_dir(self, instance): diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index bf13a4050e..1a4112107a 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -139,6 +139,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ef, instance.data["family"], instance.data["families"])) return + if "deadline" in instance.data["families"]: + return + self.integrated_file_sizes = {} try: self.register(instance) diff --git a/openpype/scripts/remote_publish.py b/openpype/scripts/remote_publish.py new file mode 100644 index 0000000000..b54c8d931b --- /dev/null +++ b/openpype/scripts/remote_publish.py @@ -0,0 +1,11 @@ +try: + from openpype.api import Logger + import openpype.lib.remote_publish +except ImportError as exc: + # Ensure Deadline fails by output an error that contains "Fatal Error:" + raise ImportError("Fatal Error: %s" % exc) + +if __name__ == "__main__": + # Perform remote publish with thorough error checking + log = Logger.get_logger(__name__) + openpype.lib.remote_publish.publish(log) From 7b65184389640165d7268996dc069600722fe60f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 17:45:15 +0200 Subject: [PATCH 084/194] OP-2787 - replaced target farm with remote Target farm is being used for rendering, this should better differentiate it. --- openpype/hosts/maya/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index b75af29523..c2fe8a95a5 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -73,8 +73,8 @@ def install(): "save/open/new callback installation..")) # Register default "local" target - print("Registering pyblish target: farm") - pyblish.api.register_target("farm") + print("Registering pyblish target: remote") + pyblish.api.register_target("remote") return print("Registering pyblish target: local") From c3e13a9e198a7082439588d6b3f6550bbdf98675 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 May 2022 18:07:26 +0200 Subject: [PATCH 085/194] modules have ability to modify environments before launch --- openpype/lib/applications.py | 33 ++++++++++++++++++++++++++++----- openpype/modules/base.py | 19 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 6ade33b59c..a81bdeca0f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1282,7 +1282,13 @@ class EnvironmentPrepData(dict): def get_app_environments_for_context( - project_name, asset_name, task_name, app_name, env_group=None, env=None + project_name, + asset_name, + task_name, + app_name, + env_group=None, + env=None, + modules_manager=None ): """Prepare environment variables by context. Args: @@ -1293,10 +1299,12 @@ def get_app_environments_for_context( by ApplicationManager. env (dict): Initial environment variables. `os.environ` is used when not passed. + modules_manager (ModulesManager): Initialized modules manager. Returns: dict: Environments for passed context and application. """ + from openpype.pipeline import AvalonMongoDB # Avalon database connection @@ -1311,6 +1319,11 @@ def get_app_environments_for_context( "name": asset_name }) + if modules_manager is None: + from openpype.modules import ModulesManager + + modules_manager = ModulesManager() + # Prepare app object which can be obtained only from ApplciationManager app_manager = ApplicationManager() app = app_manager.applications[app_name] @@ -1334,7 +1347,7 @@ def get_app_environments_for_context( "env": env }) - prepare_app_environments(data, env_group) + prepare_app_environments(data, env_group, modules_manager) prepare_context_environments(data, env_group) # Discard avalon connection @@ -1355,9 +1368,12 @@ def _merge_env(env, current_env): return result -def _add_python_version_paths(app, env, logger): +def _add_python_version_paths(app, env, logger, modules_manager): """Add vendor packages specific for a Python version.""" + for module in modules_manager.get_enabled_modules(): + module.modify_application_launch_arguments(app, env) + # Skip adding if host name is not set if not app.host_name: return @@ -1390,7 +1406,9 @@ def _add_python_version_paths(app, env, logger): env["PYTHONPATH"] = os.pathsep.join(python_paths) -def prepare_app_environments(data, env_group=None, implementation_envs=True): +def prepare_app_environments( + data, env_group=None, implementation_envs=True, modules_manager=None +): """Modify launch environments based on launched app and context. Args: @@ -1403,7 +1421,12 @@ def prepare_app_environments(data, env_group=None, implementation_envs=True): log = data["log"] source_env = data["env"].copy() - _add_python_version_paths(app, source_env, log) + if modules_manager is None: + from openpype.modules import ModulesManager + + modules_manager = ModulesManager() + + _add_python_version_paths(app, source_env, log, modules_manager) # Use environments from local settings filtered_local_envs = {} diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 5b49649359..d591df6768 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -370,6 +370,7 @@ def _load_modules(): class _OpenPypeInterfaceMeta(ABCMeta): """OpenPypeInterface meta class to print proper string.""" + def __str__(self): return "<'OpenPypeInterface.{}'>".format(self.__name__) @@ -388,6 +389,7 @@ class OpenPypeInterface: OpenPype modules which means they have to have implemented methods defined in the interface. By default interface does not have any abstract parts. """ + pass @@ -432,10 +434,12 @@ class OpenPypeModule: It is not recommended to override __init__ that's why specific method was implemented. """ + pass def connect_with_modules(self, enabled_modules): """Connect with other enabled modules.""" + pass def get_global_environments(self): @@ -443,8 +447,22 @@ class OpenPypeModule: Environment variables that can be get only from system settings. """ + return {} + def modify_application_launch_arguments(self, app, env): + """Give option to modify launch environments before application launch. + + Implementation is optional. To change environments modify passed + dictionary of environments. + + Args: + app (Application): Application that is launcher. + env (dict): Current environemnt variables. + """ + + pass + def cli(self, module_click_group): """Add commands to click group. @@ -465,6 +483,7 @@ class OpenPypeModule: def mycommand(): print("my_command") """ + pass From 6fc240734c799a37c349f7a6e8945f7feea50ab5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 20 May 2022 18:10:40 +0200 Subject: [PATCH 086/194] ftrack module modify application launch environments in module instead of in prelaunch hook --- openpype/modules/base.py | 4 +- openpype/modules/ftrack/ftrack_module.py | 34 +++++++++++++++ .../ftrack/launch_hooks/pre_python2_vendor.py | 43 ------------------- 3 files changed, 36 insertions(+), 45 deletions(-) delete mode 100644 openpype/modules/ftrack/launch_hooks/pre_python2_vendor.py diff --git a/openpype/modules/base.py b/openpype/modules/base.py index d591df6768..96c1b84a54 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -450,14 +450,14 @@ class OpenPypeModule: return {} - def modify_application_launch_arguments(self, app, env): + def modify_application_launch_arguments(self, application, env): """Give option to modify launch environments before application launch. Implementation is optional. To change environments modify passed dictionary of environments. Args: - app (Application): Application that is launcher. + application (Application): Application that is launched. env (dict): Current environemnt variables. """ diff --git a/openpype/modules/ftrack/ftrack_module.py b/openpype/modules/ftrack/ftrack_module.py index 5c38df2e03..f99e189082 100644 --- a/openpype/modules/ftrack/ftrack_module.py +++ b/openpype/modules/ftrack/ftrack_module.py @@ -88,6 +88,40 @@ class FtrackModule( """Implementation of `ILaunchHookPaths`.""" return os.path.join(FTRACK_MODULE_DIR, "launch_hooks") + def modify_application_launch_arguments(self, application, env): + if not application.use_python_2: + return + + self.log.info("Adding Ftrack Python 2 packages to PYTHONPATH.") + + # Prepare vendor dir path + python_2_vendor = os.path.join(FTRACK_MODULE_DIR, "python2_vendor") + + # Add Python 2 modules + python_paths = [ + # `python-ftrack-api` + os.path.join(python_2_vendor, "ftrack-python-api", "source"), + # `arrow` + os.path.join(python_2_vendor, "arrow"), + # `builtins` from `python-future` + # - `python-future` is strict Python 2 module that cause crashes + # of Python 3 scripts executed through OpenPype + # (burnin script etc.) + os.path.join(python_2_vendor, "builtins"), + # `backports.functools_lru_cache` + os.path.join( + python_2_vendor, "backports.functools_lru_cache" + ) + ] + + # Load PYTHONPATH from current launch context + python_path = env.get("PYTHONPATH") + if python_path: + python_paths.append(python_path) + + # Set new PYTHONPATH to launch context environments + env["PYTHONPATH"] = os.pathsep.join(python_paths) + def connect_with_modules(self, enabled_modules): for module in enabled_modules: if not hasattr(module, "get_ftrack_event_handler_paths"): diff --git a/openpype/modules/ftrack/launch_hooks/pre_python2_vendor.py b/openpype/modules/ftrack/launch_hooks/pre_python2_vendor.py deleted file mode 100644 index 0dd894bebf..0000000000 --- a/openpype/modules/ftrack/launch_hooks/pre_python2_vendor.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -from openpype.lib import PreLaunchHook -from openpype_modules.ftrack import FTRACK_MODULE_DIR - - -class PrePython2Support(PreLaunchHook): - """Add python ftrack api module for Python 2 to PYTHONPATH. - - Path to vendor modules is added to the beggining of PYTHONPATH. - """ - - def execute(self): - if not self.application.use_python_2: - return - - self.log.info("Adding Ftrack Python 2 packages to PYTHONPATH.") - - # Prepare vendor dir path - python_2_vendor = os.path.join(FTRACK_MODULE_DIR, "python2_vendor") - - # Add Python 2 modules - python_paths = [ - # `python-ftrack-api` - os.path.join(python_2_vendor, "ftrack-python-api", "source"), - # `arrow` - os.path.join(python_2_vendor, "arrow"), - # `builtins` from `python-future` - # - `python-future` is strict Python 2 module that cause crashes - # of Python 3 scripts executed through OpenPype (burnin script etc.) - os.path.join(python_2_vendor, "builtins"), - # `backports.functools_lru_cache` - os.path.join( - python_2_vendor, "backports.functools_lru_cache" - ) - ] - - # Load PYTHONPATH from current launch context - python_path = self.launch_context.env.get("PYTHONPATH") - if python_path: - python_paths.append(python_path) - - # Set new PYTHONPATH to launch context environments - self.launch_context.env["PYTHONPATH"] = os.pathsep.join(python_paths) From 38d58182fc9f7d908eca61ab573b7562d1e8af97 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 18:44:11 +0200 Subject: [PATCH 087/194] OP-2787 - added updating of script url Remote publish requires path to script which is known only on DL node. Injection of env var is required for remote publish. --- .../repository/custom/plugins/GlobalJobPreLoad.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index eeb1f7744c..bcd853f374 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -87,6 +87,13 @@ def inject_openpype_environment(deadlinePlugin): for key, value in contents.items(): deadlinePlugin.SetProcessEnvironmentVariable(key, value) + script_url = job.GetJobPluginInfoKeyValue("ScriptFilename") + if script_url: + + script_url = script_url.format(**contents).replace("\\", "/") + print(">>> Setting script path {}".format(script_url)) + job.SetJobPluginInfoKeyValue("ScriptFilename", script_url) + print(">>> Removing temporary file") os.remove(export_url) @@ -196,16 +203,19 @@ def __main__(deadlinePlugin): job.GetJobEnvironmentKeyValue('OPENPYPE_RENDER_JOB') or '0' openpype_publish_job = \ job.GetJobEnvironmentKeyValue('OPENPYPE_PUBLISH_JOB') or '0' + openpype_remote_job = \ + job.GetJobEnvironmentKeyValue('OPENPYPE_REMOTE_JOB') or '0' print("--- Job type - render {}".format(openpype_render_job)) print("--- Job type - publish {}".format(openpype_publish_job)) + print("--- Job type - remote {}".format(openpype_remote_job)) if openpype_publish_job == '1' and openpype_render_job == '1': raise RuntimeError("Misconfiguration. Job couldn't be both " + "render and publish.") if openpype_publish_job == '1': inject_render_job_id(deadlinePlugin) - elif openpype_render_job == '1': + elif openpype_render_job == '1' or openpype_remote_job == '1': inject_openpype_environment(deadlinePlugin) else: pype(deadlinePlugin) # backward compatibility with Pype2 From 529c31c4f912d66bece87a17eb597c1ec5dd86ad Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 19:02:17 +0200 Subject: [PATCH 088/194] OP-2787 - updated validator Checks for exactly 1 out set. --- .../hosts/maya/plugins/publish/validate_animation_content.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_animation_content.py b/openpype/hosts/maya/plugins/publish/validate_animation_content.py index bcea761a01..7638c44b87 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animation_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_animation_content.py @@ -30,6 +30,10 @@ class ValidateAnimationContent(pyblish.api.InstancePlugin): assert 'out_hierarchy' in instance.data, "Missing `out_hierarchy` data" + out_sets = [node for node in instance if node.endswith("out_SET")] + msg = "Couldn't find exactly one out_SET: {0}".format(out_sets) + assert len(out_sets) == 1, msg + # All nodes in the `out_hierarchy` must be among the nodes that are # in the instance. The nodes in the instance are found from the top # group, as such this tests whether all nodes are under that top group. From 62ac633da95772488f94ea4b9d53c2c5a74e9b9c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 19:05:35 +0200 Subject: [PATCH 089/194] OP-2787 - used settings from ProcessSubmittedJobOnFarm --- .../submit_maya_remote_publish_deadline.py | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 761bc8cc95..b11698f8e8 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -4,18 +4,22 @@ import requests from maya import cmds from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings import pyblish.api -class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): +class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): """Submit Maya scene to perform a local publish in Deadline. Publishing in Deadline can be helpful for scenes that publish very slow. This way it can process in the background on another machine without the Artist having to wait for the publish to finish on their local machine. - Submission is done through the Deadline Web Service. + Submission is done through the Deadline Web Service. DL then triggers + `openpype/scripts/remote_publish.py`. + + Each publishable instance creates its own full publish job. Different from `ProcessSubmittedJobOnFarm` which creates publish job depending on metadata json containing context and instance data of @@ -27,31 +31,24 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): hosts = ["maya"] families = ["deadline"] - # custom deadline attributes - deadline_department = "" - deadline_pool = "" - deadline_pool_secondary = "" - deadline_group = "" - deadline_chunk_size = 1 - deadline_priority = 50 - - def process(self, context): + def process(self, instance): + settings = get_project_settings(os.getenv("AVALON_PROJECT")) + # use setting for publish job on farm, no reason to have it separately + deadline_publish_job_sett = (settings["deadline"] + ["publish"] + ["ProcessSubmittedJobOnFarm"]) # Ensure no errors so far - assert all(result["success"] for result in context.data["results"]), ( - "Errors found, aborting integration..") + assert (all(result["success"] + for result in instance.context.data["results"]), + ("Errors found, aborting integration..")) - # Note that `publish` data member might change in the future. - # See: https://github.com/pyblish/pyblish-base/issues/307 - actives = [i for i in context if i.data["publish"]] - instance_names = sorted(instance.name for instance in actives) - - if not instance_names: + if not instance.data["publish"]: self.log.warning("No active instances found. " "Skipping submission..") return - scene = context.data["currentFile"] + scene = instance.context.data["currentFile"] scenename = os.path.basename(scene) # Get project code @@ -66,17 +63,15 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): "JobInfo": { "Plugin": "MayaBatch", "BatchName": batch_name, - "Priority": 50, "Name": job_name, - "UserName": context.data["user"], - # "Comment": instance.context.data.get("comment", ""), + "UserName": instance.context.data["user"], + "Comment": instance.context.data.get("comment", ""), # "InitialStatus": state - "Department": self.deadline_department, - "ChunkSize": self.deadline_chunk_size, - "Priority": self.deadline_priority, - - "Group": self.deadline_group, - + "Department": deadline_publish_job_sett["deadline_department"], + "ChunkSize": deadline_publish_job_sett["deadline_chunk_size"], + "Priority": deadline_publish_job_sett["deadline_priority"], + "Group": deadline_publish_job_sett["deadline_group"], + "Pool": deadline_publish_job_sett["deadline_pool"], }, "PluginInfo": { @@ -86,7 +81,7 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): # Inputs "SceneFile": scene, - "ScriptFilename": "{OPENPYPE_ROOT}/scripts/remote_publish.py", + "ScriptFilename": "{OPENPYPE_REPOS_ROOT}/openpype/scripts/remote_publish.py", # noqa # Mandatory for Deadline "Version": cmds.about(version=True), @@ -116,10 +111,9 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): environment["AVALON_TASK"] = legacy_io.Session["AVALON_TASK"] environment["AVALON_APP_NAME"] = os.environ.get("AVALON_APP_NAME") environment["OPENPYPE_LOG_NO_COLORS"] = "1" - environment["OPENPYPE_USERNAME"] = context.data["user"] - environment["OPENPYPE_PUBLISH_JOB"] = "1" - environment["OPENPYPE_RENDER_JOB"] = "0" - environment["PYBLISH_ACTIVE_INSTANCES"] = ",".join(instance_names) + environment["OPENPYPE_REMOTE_JOB"] = "1" + environment["OPENPYPE_USERNAME"] = instance.context.data["user"] + environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"] payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( @@ -129,7 +123,10 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.ContextPlugin): }) self.log.info("Submitting Deadline job ...") - deadline_url = context.data["defaultDeadline"] + deadline_url = instance.context.data["defaultDeadline"] + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") assert deadline_url, "Requires Deadline Webservice URL" url = "{}/api/jobs".format(deadline_url) response = requests.post(url, json=payload, timeout=10) From bf75d18a7b6852415cbb71b69a8a6a05c0ea3754 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 19:07:21 +0200 Subject: [PATCH 090/194] OP-2787 - added collector for remote publishable instances Filters instances from a workfile and marks only these that should be published on a farm. --- .../publish/collect_publishable_instances.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 openpype/modules/deadline/plugins/publish/collect_publishable_instances.py diff --git a/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py b/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py new file mode 100644 index 0000000000..9a467428fd --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +"""Collect instances that should be processed and published on DL. + +""" +import os + +import pyblish.api + + +class CollectDeadlinePublishableInstances(pyblish.api.InstancePlugin): + """Collect instances that should be processed and published on DL. + + Some long running publishes (not just renders) could be offloaded to DL, + this plugin compares theirs name against env variable, marks only + publishable by farm. + + Triggered only when running only in headless mode, eg on a farm. + """ + + order = pyblish.api.CollectorOrder + 0.499 + label = "Collect Deadline Publishable Instance" + targets = ["remote"] + + def process(self, instance): + self.log.debug("CollectDeadlinePublishableInstances") + publish_inst = os.environ.get("OPENPYPE_PUBLISH_SUBSET", '') + assert (publish_inst, + "OPENPYPE_PUBLISH_SUBSET env var required for " + "remote publishing") + + subset_name = instance.data["subset"] + if subset_name == publish_inst: + self.log.debug("Publish {}".format(subset_name)) + instance.data["publish"] = True + instance.data["farm"] = False + instance.data["families"].remove("deadline") + else: + self.log.debug("Skipping {}".format(subset_name)) + instance.data["publish"] = False From 72d8633266048ba19b78d425c879aa0325ba042b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 19:09:35 +0200 Subject: [PATCH 091/194] OP-2787 - changed flag from family to farm It probably makes more sense to check specific flag than a family. --- openpype/plugins/publish/integrate_new.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 1a4112107a..b5a7f11904 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -139,7 +139,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): ef, instance.data["family"], instance.data["families"])) return - if "deadline" in instance.data["families"]: + # instance should be published on a farm + if instance.data["farm"]: return self.integrated_file_sizes = {} From 6b71ff1909c3d32e5bebc0595580f1dcde1c1180 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 20 May 2022 19:25:55 +0200 Subject: [PATCH 092/194] OP-2787 - removed deadline family deadline family is not used anymore anywhere, filtering on integrate is being done on instance.data["farm"] flag. --- .../maya/plugins/publish/collect_animation.py | 3 --- .../maya/plugins/publish/collect_pointcache.py | 14 -------------- 2 files changed, 17 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/collect_pointcache.py diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index b442113fbc..9b1e38fd0a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -55,6 +55,3 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): # Store data in the instance for the validator instance.data["out_hierarchy"] = hierarchy - - if instance.data.get("farm"): - instance.data["families"].append("deadline") diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py deleted file mode 100644 index b55babe372..0000000000 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ /dev/null @@ -1,14 +0,0 @@ -import pyblish.api - - -class CollectPointcache(pyblish.api.InstancePlugin): - """Collect pointcache data for instance.""" - - order = pyblish.api.CollectorOrder + 0.4 - families = ["pointcache"] - label = "Collect Pointcache" - hosts = ["maya"] - - def process(self, instance): - if instance.data.get("farm"): - instance.data["families"].append("deadline") \ No newline at end of file From 9244389b585b654f92dbf76a8e5ed2692ac4cb3b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 23 May 2022 17:16:44 +0200 Subject: [PATCH 093/194] OP-2790 - safer querying of farm flag --- openpype/plugins/publish/integrate_new.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index b5a7f11904..fa0582c65a 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -140,7 +140,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): return # instance should be published on a farm - if instance.data["farm"]: + if instance.data.get("farm"): return self.integrated_file_sizes = {} From c9c6fe34fe63fff1e4ba57d73bab6c361e7203ca Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 10:42:37 +0200 Subject: [PATCH 094/194] OP-2787 - changed assert to PublishXmlValidationError --- .../help/submit_maya_remote_publish_deadline.xml | 16 ++++++++++++++++ .../submit_maya_remote_publish_deadline.py | 8 ++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/help/submit_maya_remote_publish_deadline.xml diff --git a/openpype/hosts/maya/plugins/publish/help/submit_maya_remote_publish_deadline.xml b/openpype/hosts/maya/plugins/publish/help/submit_maya_remote_publish_deadline.xml new file mode 100644 index 0000000000..e92320ccdc --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/help/submit_maya_remote_publish_deadline.xml @@ -0,0 +1,16 @@ + + + +Errors found + +## Publish process has errors + +At least one plugin failed before this plugin, job won't be sent to Deadline for processing before all issues are fixed. + +### How to repair? + +Check all failing plugins (should be highlighted in red) and fix issues if possible. + + + + \ No newline at end of file diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index b11698f8e8..be8c50d7b3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -3,7 +3,7 @@ import requests from maya import cmds -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, PublishXmlValidationError from openpype.settings import get_project_settings import pyblish.api @@ -39,9 +39,9 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): ["ProcessSubmittedJobOnFarm"]) # Ensure no errors so far - assert (all(result["success"] - for result in instance.context.data["results"]), - ("Errors found, aborting integration..")) + if not (all(result["success"] + for result in instance.context.data["results"])): + raise PublishXmlValidationError("Publish process has errors") if not instance.data["publish"]: self.log.warning("No active instances found. " From 4ca419f0b63c2782d8955b8012674ca131a57ad9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 11:36:49 +0200 Subject: [PATCH 095/194] Revert "OP-2787 - removed deadline family" This reverts commit 6b71ff19 --- .../maya/plugins/publish/collect_animation.py | 3 +++ .../maya/plugins/publish/collect_pointcache.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/collect_pointcache.py diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index 9b1e38fd0a..b442113fbc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -55,3 +55,6 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): # Store data in the instance for the validator instance.data["out_hierarchy"] = hierarchy + + if instance.data.get("farm"): + instance.data["families"].append("deadline") diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py new file mode 100644 index 0000000000..b55babe372 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -0,0 +1,14 @@ +import pyblish.api + + +class CollectPointcache(pyblish.api.InstancePlugin): + """Collect pointcache data for instance.""" + + order = pyblish.api.CollectorOrder + 0.4 + families = ["pointcache"] + label = "Collect Pointcache" + hosts = ["maya"] + + def process(self, instance): + if instance.data.get("farm"): + instance.data["families"].append("deadline") \ No newline at end of file From c96ea856425550835c7c5dfc42b1965a54ca0902 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 11:40:19 +0200 Subject: [PATCH 096/194] OP-2787 - change families to more generic Not only Deadline could be used for remote publish. DL plugin will be picked only if DL module is enabled. --- openpype/hosts/maya/plugins/publish/collect_animation.py | 2 +- openpype/hosts/maya/plugins/publish/collect_pointcache.py | 2 +- .../plugins/publish/submit_maya_remote_publish_deadline.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index b442113fbc..549098863f 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -57,4 +57,4 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): instance.data["out_hierarchy"] = hierarchy if instance.data.get("farm"): - instance.data["families"].append("deadline") + instance.data["families"].append("publish.farm") diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index b55babe372..a841341f72 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -11,4 +11,4 @@ class CollectPointcache(pyblish.api.InstancePlugin): def process(self, instance): if instance.data.get("farm"): - instance.data["families"].append("deadline") \ No newline at end of file + instance.data["families"].append("publish.farm") diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index be8c50d7b3..210fefb520 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -29,7 +29,7 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): label = "Submit Scene to Deadline" order = pyblish.api.IntegratorOrder hosts = ["maya"] - families = ["deadline"] + families = ["publish.farm"] def process(self, instance): settings = get_project_settings(os.getenv("AVALON_PROJECT")) From 0d03e3e2f836476310b899a21aec5b49dbd2a7c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 11:40:50 +0200 Subject: [PATCH 097/194] OP-2787 - removed obsolete part of code Doesn't do anything. --- openpype/hosts/maya/plugins/publish/extract_animation.py | 7 +------ openpype/hosts/maya/plugins/publish/extract_pointcache.py | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index 87f2d35192..1ccc8f5cfe 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -24,12 +24,7 @@ class ExtractAnimation(openpype.api.Extractor): def process(self, instance): if instance.data.get("farm"): - path = os.path.join( - os.path.dirname(instance.context.data["currentFile"]), - "cache", - instance.data["name"] + ".abc" - ) - instance.data["expectedFiles"] = [os.path.normpath(path)] + self.log.debug("Should be processed on farm, skipping.") return # Collect the out set nodes diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 7ad4c6dfa9..ff3d97ded1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -26,12 +26,7 @@ class ExtractAlembic(openpype.api.Extractor): def process(self, instance): if instance.data.get("farm"): - path = os.path.join( - os.path.dirname(instance.context.data["currentFile"]), - "cache", - instance.data["name"] + ".abc" - ) - instance.data["expectedFiles"] = [os.path.normpath(path)] + self.log.debug("Should be processed on farm, skipping.") return nodes = instance[:] From de161da68b4035c4fde2e376dc8da1ab2b79d093 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 11:45:29 +0200 Subject: [PATCH 098/194] OP-2787 - created explicit env var HEADLESS_PUBLISH Env var created to differentiate launch of Maya on the farm. lib.IS_HEADLESS might be triggered locally, it is not precise enough. Added same env var to all commands to standardize it a bit. --- openpype/hosts/maya/api/pipeline.py | 5 ++++- .../submit_maya_remote_publish_deadline.py | 2 ++ openpype/pype_commands.py | 18 +++++++++++++----- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index c2fe8a95a5..6fc93e864f 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -72,7 +72,10 @@ def install(): log.info(("Running in headless mode, skipping Maya " "save/open/new callback installation..")) - # Register default "local" target + return + + if os.environ.get("HEADLESS_PUBLISH"): + # Maya launched on farm, lib.IS_HEADLESS might be triggered locally too print("Registering pyblish target: remote") pyblish.api.register_target("remote") return diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 210fefb520..8f50878db4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -114,6 +114,8 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): environment["OPENPYPE_REMOTE_JOB"] = "1" environment["OPENPYPE_USERNAME"] = instance.context.data["user"] environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"] + environment["HEADLESS_PUBLISH"] = "1" + payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index d945a1f697..90c582a319 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -144,6 +144,7 @@ class PypeCommands: pyblish.api.register_target("farm") os.environ["OPENPYPE_PUBLISH_DATA"] = os.pathsep.join(paths) + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib log.info("Running publish ...") @@ -173,9 +174,11 @@ class PypeCommands: user_email, targets=None): """Opens installed variant of 'host' and run remote publish there. + Eventually should be yanked out to Webpublisher cli. + Currently implemented and tested for Photoshop where customer wants to process uploaded .psd file and publish collected layers - from there. + from there. Triggered by Webpublisher. Checks if no other batches are running (status =='in_progress). If so, it sleeps for SLEEP (this is separate process), @@ -273,7 +276,8 @@ class PypeCommands: def remotepublish(project, batch_path, user_email, targets=None): """Start headless publishing. - Used to publish rendered assets, workfiles etc. + Used to publish rendered assets, workfiles etc via Webpublisher. + Eventually should be yanked out to Webpublisher cli. Publish use json from passed paths argument. @@ -309,6 +313,7 @@ class PypeCommands: os.environ["AVALON_PROJECT"] = project os.environ["AVALON_APP"] = host_name os.environ["USER_EMAIL"] = user_email + os.environ["HEADLESS_PUBLISH"] = 'true' # to use in app lib pyblish.api.register_host(host_name) @@ -331,9 +336,12 @@ class PypeCommands: log.info("Publish finished.") @staticmethod - def extractenvironments( - output_json_path, project, asset, task, app, env_group - ): + def extractenvironments(output_json_path, project, asset, task, app, + env_group): + """Produces json file with environment based on project and app. + + Called by Deadline plugin to propagate environment into render jobs. + """ if all((project, asset, task, app)): from openpype.api import get_app_environments_for_context env = get_app_environments_for_context( From 8cda0ebbeef23ceccc7f1a9d9963eecc93219012 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 May 2022 12:09:13 +0200 Subject: [PATCH 099/194] OP-2787 - Hound --- .../plugins/publish/collect_publishable_instances.py | 7 ++++--- .../plugins/publish/submit_maya_remote_publish_deadline.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py b/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py index 9a467428fd..741a2a5af8 100644 --- a/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py +++ b/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py @@ -5,6 +5,7 @@ import os import pyblish.api +from openpype.pipeline import PublishValidationError class CollectDeadlinePublishableInstances(pyblish.api.InstancePlugin): @@ -24,9 +25,9 @@ class CollectDeadlinePublishableInstances(pyblish.api.InstancePlugin): def process(self, instance): self.log.debug("CollectDeadlinePublishableInstances") publish_inst = os.environ.get("OPENPYPE_PUBLISH_SUBSET", '') - assert (publish_inst, - "OPENPYPE_PUBLISH_SUBSET env var required for " - "remote publishing") + if not publish_inst: + raise PublishValidationError("OPENPYPE_PUBLISH_SUBSET env var " + "required for remote publishing") subset_name = instance.data["subset"] if subset_name == publish_inst: diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 8f50878db4..196adc5906 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -116,7 +116,6 @@ class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): environment["OPENPYPE_PUBLISH_SUBSET"] = instance.data["subset"] environment["HEADLESS_PUBLISH"] = "1" - payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, From 67fdfba49f35b585ad5e391271fb6ebe5e43f2d3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 May 2022 11:01:40 +0200 Subject: [PATCH 100/194] OP-2790 - added note about remote publish to documentation --- website/docs/artist_hosts_maya.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 73e89384e8..48e1093753 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -312,6 +312,10 @@ Example setup: ![Maya - Point Cache Example](assets/maya-pointcache_setup.png) +:::note Publish on farm +If your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. +Only thing that is necessary is to toggle `Farm` property in created pointcache instance to True. + ### Loading Point Caches Loading point cache means creating reference to **abc** file with Go **OpenPype → Load...**. From d404fbf8a285e0e25f5af3d8695c6df547f0ceab Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 30 May 2022 11:51:19 +0200 Subject: [PATCH 101/194] OP-3277 - added functionality to replace root value with environment variable. Useful for remote workflows where Site Sync is being used. When Load reference is used, real root value (c:/project) is replaced with ${OPENPYPE_ROOT_WORK}. --- openpype/hosts/maya/plugins/load/load_reference.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index d65b5a2c1e..7fa7362ecc 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -8,6 +8,7 @@ from openpype.pipeline import ( legacy_create, ) import openpype.hosts.maya.api.plugin +from openpype.api import Anatomy from openpype.hosts.maya.api.lib import maintained_selection @@ -51,7 +52,9 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) - nodes = cmds.file(self.fname, + anatomy = Anatomy(context["project"]["code"]) + file_url = anatomy.replace_root_with_env_key(self.fname, '${{{}}}') + nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, reference=True, From 277024de81843a3198c47402e7b251f937ab0817 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 30 May 2022 13:23:24 +0200 Subject: [PATCH 102/194] OP-3277 - extracted logic to ReferenceLoader All inheriting plugins implementing shared method. Flag 'use_env_var_as_root' set to True for testing temporarily, proper location in Setting should be decided. --- openpype/hosts/maya/api/plugin.py | 25 +++++++++++++++++++ .../maya/plugins/load/_load_animation.py | 5 ++-- openpype/hosts/maya/plugins/load/load_ass.py | 12 ++++++--- openpype/hosts/maya/plugins/load/load_look.py | 4 ++- .../hosts/maya/plugins/load/load_reference.py | 5 ++-- .../hosts/maya/plugins/load/load_yeti_rig.py | 4 ++- 6 files changed, 45 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 3721868823..93b0793d9c 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -10,6 +10,7 @@ from openpype.pipeline import ( get_representation_path, AVALON_CONTAINER_ID, ) +from openpype.api import Anatomy from .pipeline import containerise from . import lib @@ -132,6 +133,7 @@ class ReferenceLoader(Loader): " imported representation ?" ) ] + use_env_var_as_root = True def load( self, @@ -191,6 +193,25 @@ class ReferenceLoader(Loader): """To be implemented by subclass""" raise NotImplementedError("Must be implemented by subclass") + def prepare_root_value(self, file_url, project_name): + """Replace root value with env var placeholder. + + Use ${OPENPYPE_ROOT_WORK} (or any other root) instead of proper root + value when storing referenced url into a workfile. + Useful for remote workflows with SiteSync. + + Args: + file_url (str) + project_name (dict) + Returns: + (str) + """ + if self.use_env_var_as_root: + anatomy = Anatomy(project_name) + file_url = anatomy.replace_root_with_env_key(file_url, '${{{}}}') + + return file_url + def update(self, container, representation): from maya import cmds from openpype.hosts.maya.api.lib import get_container_members @@ -230,6 +251,10 @@ class ReferenceLoader(Loader): self.log.debug("No alembic nodes found in {}".format(members)) try: + path = self.prepare_root_value(path, + representation["context"] + ["project"] + ["code"]) content = cmds.file(path, loadReference=reference_node, type=file_type, diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 9c37e498ef..0010efb829 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -35,8 +35,9 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # hero_001 (abc) # asset_counter{optional} - - nodes = cmds.file(self.fname, + file_url = self.prepare_root_value(self.fname, + context["project"]["code"]) + nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, groupReference=True, diff --git a/openpype/hosts/maya/plugins/load/load_ass.py b/openpype/hosts/maya/plugins/load/load_ass.py index a284b7ec1f..1f0eb88995 100644 --- a/openpype/hosts/maya/plugins/load/load_ass.py +++ b/openpype/hosts/maya/plugins/load/load_ass.py @@ -64,9 +64,11 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): path = os.path.join(publish_folder, filename) proxyPath = proxyPath_base + ".ma" - self.log.info - nodes = cmds.file(proxyPath, + file_url = self.prepare_root_value(proxyPath, + context["project"]["code"]) + + nodes = cmds.file(file_url, namespace=namespace, reference=True, returnNewNodes=True, @@ -123,7 +125,11 @@ class AssProxyLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): assert os.path.exists(proxyPath), "%s does not exist." % proxyPath try: - content = cmds.file(proxyPath, + file_url = self.prepare_root_value(proxyPath, + representation["context"] + ["project"] + ["code"]) + content = cmds.file(file_url, loadReference=reference_node, type="mayaAscii", returnNewNodes=True) diff --git a/openpype/hosts/maya/plugins/load/load_look.py b/openpype/hosts/maya/plugins/load/load_look.py index 80eac8e0b5..ae3a683241 100644 --- a/openpype/hosts/maya/plugins/load/load_look.py +++ b/openpype/hosts/maya/plugins/load/load_look.py @@ -31,7 +31,9 @@ class LookLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): import maya.cmds as cmds with lib.maintained_selection(): - nodes = cmds.file(self.fname, + file_url = self.prepare_root_value(self.fname, + context["project"]["code"]) + nodes = cmds.file(file_url, namespace=namespace, reference=True, returnNewNodes=True) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 7fa7362ecc..e4355ed3d4 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -8,7 +8,6 @@ from openpype.pipeline import ( legacy_create, ) import openpype.hosts.maya.api.plugin -from openpype.api import Anatomy from openpype.hosts.maya.api.lib import maintained_selection @@ -52,8 +51,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): with maintained_selection(): cmds.loadPlugin("AbcImport.mll", quiet=True) - anatomy = Anatomy(context["project"]["code"]) - file_url = anatomy.replace_root_with_env_key(self.fname, '${{{}}}') + file_url = self.prepare_root_value(self.fname, + context["project"]["code"]) nodes = cmds.file(file_url, namespace=namespace, sharedReferenceFile=False, diff --git a/openpype/hosts/maya/plugins/load/load_yeti_rig.py b/openpype/hosts/maya/plugins/load/load_yeti_rig.py index b4d31b473f..241c28467a 100644 --- a/openpype/hosts/maya/plugins/load/load_yeti_rig.py +++ b/openpype/hosts/maya/plugins/load/load_yeti_rig.py @@ -53,7 +53,9 @@ class YetiRigLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): # load rig with lib.maintained_selection(): - nodes = cmds.file(self.fname, + file_url = self.prepare_root_value(self.fname, + context["project"]["code"]) + nodes = cmds.file(file_url, namespace=namespace, reference=True, returnNewNodes=True, From 38c8d2c8fe63c81fbc55c4f079e4f72519575c3c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 30 May 2022 15:27:15 +0200 Subject: [PATCH 103/194] Fix udim support for e.g. uppercase tag --- openpype/hosts/maya/plugins/publish/collect_look.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_look.py b/openpype/hosts/maya/plugins/publish/collect_look.py index 323bede761..dc17ddc605 100644 --- a/openpype/hosts/maya/plugins/publish/collect_look.py +++ b/openpype/hosts/maya/plugins/publish/collect_look.py @@ -109,16 +109,18 @@ def node_uses_image_sequence(node, node_path): """ # useFrameExtension indicates an explicit image sequence - # The following tokens imply a sequence - patterns = ["", "", "", - "u_v", ""] try: use_frame_extension = cmds.getAttr('%s.useFrameExtension' % node) except ValueError: use_frame_extension = False + if use_frame_extension: + return True - return (use_frame_extension or - any(pattern in node_path for pattern in patterns)) + # The following tokens imply a sequence + patterns = ["", "", "", + "u_v", ""] + node_path_lowered = node_path.lower() + return any(pattern in node_path_lowered for pattern in patterns) def seq_to_glob(path): From 254455e786c50ef20d0f953bebd57d50af077b1e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 30 May 2022 18:05:56 +0200 Subject: [PATCH 104/194] OP-3277 - introduced Settings variable Configured via Maya/Maya-dirmap to use in all Loaders --- openpype/hosts/maya/api/plugin.py | 45 ++++++++++--------- .../defaults/project_settings/maya.json | 1 + .../projects_schema/schema_project_maya.json | 6 +++ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 93b0793d9c..f05893a7b4 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -11,7 +11,7 @@ from openpype.pipeline import ( AVALON_CONTAINER_ID, ) from openpype.api import Anatomy - +from openpype.settings import get_project_settings from .pipeline import containerise from . import lib @@ -133,7 +133,6 @@ class ReferenceLoader(Loader): " imported representation ?" ) ] - use_env_var_as_root = True def load( self, @@ -193,25 +192,6 @@ class ReferenceLoader(Loader): """To be implemented by subclass""" raise NotImplementedError("Must be implemented by subclass") - def prepare_root_value(self, file_url, project_name): - """Replace root value with env var placeholder. - - Use ${OPENPYPE_ROOT_WORK} (or any other root) instead of proper root - value when storing referenced url into a workfile. - Useful for remote workflows with SiteSync. - - Args: - file_url (str) - project_name (dict) - Returns: - (str) - """ - if self.use_env_var_as_root: - anatomy = Anatomy(project_name) - file_url = anatomy.replace_root_with_env_key(file_url, '${{{}}}') - - return file_url - def update(self, container, representation): from maya import cmds from openpype.hosts.maya.api.lib import get_container_members @@ -344,6 +324,29 @@ class ReferenceLoader(Loader): except RuntimeError: pass + def prepare_root_value(self, file_url, project_name): + """Replace root value with env var placeholder. + + Use ${OPENPYPE_ROOT_WORK} (or any other root) instead of proper root + value when storing referenced url into a workfile. + Useful for remote workflows with SiteSync. + + Args: + file_url (str) + project_name (dict) + Returns: + (str) + """ + settings = get_project_settings(project_name) + use_env_var_as_root = (settings["maya"] + ["maya-dirmap"] + ["use_env_var_as_root"]) + if use_env_var_as_root: + anatomy = Anatomy(project_name) + file_url = anatomy.replace_root_with_env_key(file_url, '${{{}}}') + + return file_url + @staticmethod def _organize_containers(nodes, container): # type: (list, str) -> None diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index e03bdcecc3..a42f889e85 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -8,6 +8,7 @@ "yetiRig": "ma" }, "maya-dirmap": { + "use_env_var_as_root": true, "enabled": false, "paths": { "source-path": [], diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 0c7943447b..f9523b1baa 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -22,6 +22,12 @@ "label": "Maya Directory Mapping", "is_group": true, "children": [ + { + "type": "boolean", + "key": "use_env_var_as_root", + "label": "Use env var placeholder in referenced url", + "docstring": "Use ${} placeholder instead of physical value of root when storing into workfile metadata." + }, { "type": "boolean", "key": "enabled", From 45e0e39ea1e262044e1764f546943f4b436dc926 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 31 May 2022 11:39:03 +0200 Subject: [PATCH 105/194] :sparkles: add support for skeletalMesh and staticMesh to loaders --- openpype/hosts/unreal/plugins/load/load_rig.py | 2 +- openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index c27bd23aaf..227c5c9292 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -14,7 +14,7 @@ import unreal # noqa class SkeletalMeshFBXLoader(plugin.Loader): """Load Unreal SkeletalMesh from FBX.""" - families = ["rig"] + families = ["rig", "skeletalMesh"] label = "Import FBX Skeletal Mesh" representations = ["fbx"] icon = "cube" diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index 282d249947..351c686095 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -14,7 +14,7 @@ import unreal # noqa class StaticMeshFBXLoader(plugin.Loader): """Load Unreal StaticMesh from FBX.""" - families = ["model", "unrealStaticMesh"] + families = ["model", "staticMesh"] label = "Import FBX Static Mesh" representations = ["fbx"] icon = "cube" From 5464fd40850ae1d25c1b467149249bd5edaaae9e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 13:58:11 +0200 Subject: [PATCH 106/194] OP-2787 - fix extractors could be run on a farm --- openpype/hosts/maya/plugins/publish/extract_animation.py | 3 +++ openpype/hosts/maya/plugins/publish/extract_pointcache.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index 1ccc8f5cfe..8f2bc26d08 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -16,11 +16,14 @@ class ExtractAnimation(openpype.api.Extractor): Positions and normals, uvs, creases are preserved, but nothing more, for plain and predictable point caches. + Plugin can run locally or remotely (on a farm - if instance is marked with + "farm" it will be skipped in local processing, but processed on farm) """ label = "Extract Animation" hosts = ["maya"] families = ["animation"] + targets = ["local", "remote"] def process(self, instance): if instance.data.get("farm"): diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index ff3d97ded1..5606ea9459 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -16,6 +16,8 @@ class ExtractAlembic(openpype.api.Extractor): Positions and normals, uvs, creases are preserved, but nothing more, for plain and predictable point caches. + Plugin can run locally or remotely (on a farm - if instance is marked with + "farm" it will be skipped in local processing, but processed on farm) """ label = "Extract Pointcache (Alembic)" @@ -23,6 +25,7 @@ class ExtractAlembic(openpype.api.Extractor): families = ["pointcache", "model", "vrayproxy"] + targets = ["local", "remote"] def process(self, instance): if instance.data.get("farm"): From 376ddc41329206173f2d8f4e9d568e0aef4cebc3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 14:01:23 +0200 Subject: [PATCH 107/194] OP-2787 - added raising error for Deadline --- openpype/lib/remote_publish.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index da2497e1a5..d7884d0200 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -60,7 +60,7 @@ def start_webpublish_log(dbcon, batch_id, user): }).inserted_id -def publish(log, close_plugin_name=None): +def publish(log, close_plugin_name=None, raise_error=False): """Loops through all plugins, logs to console. Used for tests. Args: @@ -79,10 +79,15 @@ def publish(log, close_plugin_name=None): result["plugin"].label, record.msg)) if result["error"]: - log.error(error_format.format(**result)) + error_message = error_format.format(**result) + log.error(error_message) if close_plugin: # close host app explicitly after error context = pyblish.api.Context() close_plugin().process(context) + if raise_error: + # Fatal Error is because of Deadline + error_message = "Fatal Error: " + error_format.format(**result) + raise RuntimeError(error_message) def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): From 8de1cbf7320f792e0d6cf8e2709f918a9d8c4ddd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 14:15:05 +0200 Subject: [PATCH 108/194] OP-2787 - fixed resolution order --- openpype/hosts/maya/api/pipeline.py | 16 ++++++++-------- openpype/scripts/remote_publish.py | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 6fc93e864f..0261694be2 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -66,20 +66,20 @@ def install(): log.info("Installing callbacks ... ") register_event_callback("init", on_init) - # Callbacks below are not required for headless mode, the `init` however - # is important to load referenced Alembics correctly at rendertime. + if os.environ.get("HEADLESS_PUBLISH"): + # Maya launched on farm, lib.IS_HEADLESS might be triggered locally too + # target "farm" == rendering on farm, expects OPENPYPE_PUBLISH_DATA + # target "remote" == remote execution + print("Registering pyblish target: remote") + pyblish.api.register_target("remote") + return + if lib.IS_HEADLESS: log.info(("Running in headless mode, skipping Maya " "save/open/new callback installation..")) return - if os.environ.get("HEADLESS_PUBLISH"): - # Maya launched on farm, lib.IS_HEADLESS might be triggered locally too - print("Registering pyblish target: remote") - pyblish.api.register_target("remote") - return - print("Registering pyblish target: local") pyblish.api.register_target("local") diff --git a/openpype/scripts/remote_publish.py b/openpype/scripts/remote_publish.py index b54c8d931b..8e5c91d663 100644 --- a/openpype/scripts/remote_publish.py +++ b/openpype/scripts/remote_publish.py @@ -1,6 +1,7 @@ try: from openpype.api import Logger import openpype.lib.remote_publish + import pyblish.api except ImportError as exc: # Ensure Deadline fails by output an error that contains "Fatal Error:" raise ImportError("Fatal Error: %s" % exc) @@ -8,4 +9,4 @@ except ImportError as exc: if __name__ == "__main__": # Perform remote publish with thorough error checking log = Logger.get_logger(__name__) - openpype.lib.remote_publish.publish(log) + openpype.lib.remote_publish.publish(log, raise_error=True) From 2dd79b32e7ebbc3caaf863484806569bf62ab1f3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 14:15:25 +0200 Subject: [PATCH 109/194] OP-2787 - removed unnecessary family --- .../deadline/plugins/publish/collect_publishable_instances.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py b/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py index 741a2a5af8..b00381b6cf 100644 --- a/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py +++ b/openpype/modules/deadline/plugins/publish/collect_publishable_instances.py @@ -34,7 +34,6 @@ class CollectDeadlinePublishableInstances(pyblish.api.InstancePlugin): self.log.debug("Publish {}".format(subset_name)) instance.data["publish"] = True instance.data["farm"] = False - instance.data["families"].remove("deadline") else: self.log.debug("Skipping {}".format(subset_name)) instance.data["publish"] = False From d37815467a059cfa2ad2dbde6ce4025ec00def2d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 14:20:50 +0200 Subject: [PATCH 110/194] OP-2787 - added extracted path to explicit cleanup --- openpype/hosts/maya/plugins/publish/extract_animation.py | 2 ++ openpype/hosts/maya/plugins/publish/extract_pointcache.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_animation.py b/openpype/hosts/maya/plugins/publish/extract_animation.py index 8f2bc26d08..abe5ed3bf5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_animation.py @@ -95,4 +95,6 @@ class ExtractAnimation(openpype.api.Extractor): } instance.data["representations"].append(representation) + instance.context.data["cleanupFullPaths"].append(path) + self.log.info("Extracted {} to {}".format(instance, dirname)) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 5606ea9459..c4c8610ebb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -98,4 +98,6 @@ class ExtractAlembic(openpype.api.Extractor): } instance.data["representations"].append(representation) + instance.context.data["cleanupFullPaths"].append(path) + self.log.info("Extracted {} to {}".format(instance, dirname)) From 0d7d43316b94685f78fb0a7e2e39153a98996b36 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 14:44:37 +0200 Subject: [PATCH 111/194] OP-2787 - changed class to api.Integrator This plugin should run only locally, not no a farm. --- .../plugins/publish/submit_maya_remote_publish_deadline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py index 196adc5906..c31052be07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_remote_publish_deadline.py @@ -5,11 +5,12 @@ from maya import cmds from openpype.pipeline import legacy_io, PublishXmlValidationError from openpype.settings import get_project_settings +import openpype.api import pyblish.api -class MayaSubmitRemotePublishDeadline(pyblish.api.InstancePlugin): +class MayaSubmitRemotePublishDeadline(openpype.api.Integrator): """Submit Maya scene to perform a local publish in Deadline. Publishing in Deadline can be helpful for scenes that publish very slow. From 55a69074c6b550b4e30d99b658b9ec664d30214b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 14:46:05 +0200 Subject: [PATCH 112/194] OP-2787 - Hound --- openpype/scripts/remote_publish.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/scripts/remote_publish.py b/openpype/scripts/remote_publish.py index 8e5c91d663..d322f369d1 100644 --- a/openpype/scripts/remote_publish.py +++ b/openpype/scripts/remote_publish.py @@ -1,7 +1,6 @@ try: from openpype.api import Logger import openpype.lib.remote_publish - import pyblish.api except ImportError as exc: # Ensure Deadline fails by output an error that contains "Fatal Error:" raise ImportError("Fatal Error: %s" % exc) From b073853a0b1d056a0976b5a3f983184bfb195d2e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 18:58:13 +0200 Subject: [PATCH 113/194] Update openpype/settings/entities/schemas/projects_schema/schema_project_maya.json Co-authored-by: Milan Kolar --- .../entities/schemas/projects_schema/schema_project_maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index f9523b1baa..f7d92c385e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -25,7 +25,7 @@ { "type": "boolean", "key": "use_env_var_as_root", - "label": "Use env var placeholder in referenced url", + "label": "Use env var placeholder in referenced paths", "docstring": "Use ${} placeholder instead of physical value of root when storing into workfile metadata." }, { From 6530fbd918f795077f0a715827e101c97b4bc8ea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 31 May 2022 18:58:28 +0200 Subject: [PATCH 114/194] Update openpype/settings/entities/schemas/projects_schema/schema_project_maya.json Co-authored-by: Milan Kolar --- .../entities/schemas/projects_schema/schema_project_maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index f7d92c385e..40e98b0333 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -26,7 +26,7 @@ "type": "boolean", "key": "use_env_var_as_root", "label": "Use env var placeholder in referenced paths", - "docstring": "Use ${} placeholder instead of physical value of root when storing into workfile metadata." + "docstring": "Use ${} placeholder instead of absolute value of a root in referenced filepaths." }, { "type": "boolean", From a10bbbcc79b5a369d51bb4716164611edcb4dbfa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Jun 2022 12:33:09 +0200 Subject: [PATCH 115/194] OP-3068 - better handling of legacy review subsets in Maya Multiple reviews from a Maya workfile are blocked by legacy subset names without variant. These names could be used in later process so we cannot replace them. Unique subset name validator was added, check for existing subset in DB too. --- .../maya/plugins/publish/collect_review.py | 17 +++++---- .../validate_review_subset_uniqueness.xml | 28 +++++++++++++++ .../validate_review_subset_uniqueness.py | 36 +++++++++++++++++++ 3 files changed, 72 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/help/validate_review_subset_uniqueness.xml create mode 100644 openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 1af92c3bfc..e9e0d74c03 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -77,15 +77,14 @@ class CollectReview(pyblish.api.InstancePlugin): instance.data['remove'] = True self.log.debug('isntance data {}'.format(instance.data)) else: - if self.legacy: - instance.data['subset'] = task + 'Review' - else: - subset = "{}{}{}".format( - task, - instance.data["subset"][0].upper(), - instance.data["subset"][1:] - ) - instance.data['subset'] = subset + legacy_subset_name = task + 'Review' + asset_doc_id = instance.context.data['assetEntity']["_id"] + subsets = legacy_io.find({"type": "subset", + "name": legacy_subset_name, + "parent": asset_doc_id}).distinct("_id") + if len(list(subsets)) > 0: + self.log.debug("Existing subsets found, keep legacy name.") + instance.data['subset'] = legacy_subset_name instance.data['review_camera'] = camera instance.data['frameStartFtrack'] = \ diff --git a/openpype/hosts/maya/plugins/publish/help/validate_review_subset_uniqueness.xml b/openpype/hosts/maya/plugins/publish/help/validate_review_subset_uniqueness.xml new file mode 100644 index 0000000000..fd1bf4cbaa --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/help/validate_review_subset_uniqueness.xml @@ -0,0 +1,28 @@ + + + + Review subsets not unique + + ## Non unique subset name found + + Non unique subset names: '{non_unique}' + + ### __Detailed Info__ (optional) + + This might happen if you already published for this asset + review subset with legacy name {task}Review. + This legacy name limits possibility of publishing of multiple + reviews from a single workfile. Proper review subset name should + now + contain variant also (as 'Main', 'Default' etc.). That would + result in completely new subset though, so this situation must + be handled manually. + + ### How to repair? + + Legacy subsets must be removed from Openpype DB, please ask admin + to do that. Please provide them asset and subset names. + + + + \ No newline at end of file diff --git a/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py b/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py new file mode 100644 index 0000000000..d70096ee45 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_review_subset_uniqueness.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +import collections +import pyblish.api +import openpype.api +from openpype.pipeline import PublishXmlValidationError + + +class ValidateReviewSubsetUniqueness(pyblish.api.ContextPlugin): + """Validates that nodes has common root.""" + + order = openpype.api.ValidateContentsOrder + hosts = ["maya"] + families = ["review"] + label = "Validate Review Subset Unique" + + def process(self, context): + subset_names = [] + + for instance in context: + self.log.info("instance:: {}".format(instance.data)) + if instance.data.get('publish'): + subset_names.append(instance.data.get('subset')) + + non_unique = \ + [item + for item, count in collections.Counter(subset_names).items() + if count > 1] + msg = ("Instance subset names {} are not unique. ".format(non_unique) + + "Ask admin to remove subset from DB for multiple reviews.") + formatting_data = { + "non_unique": ",".join(non_unique) + } + + if non_unique: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) From 6a5fa89348bb822e1f2c8748fa867f19e2f70a8d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Jun 2022 14:27:55 +0200 Subject: [PATCH 116/194] Fix - change default of use_env_var_as_root True was there only for testing, false is more sane default. --- openpype/settings/defaults/project_settings/maya.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a42f889e85..efd22e13c8 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -8,7 +8,7 @@ "yetiRig": "ma" }, "maya-dirmap": { - "use_env_var_as_root": true, + "use_env_var_as_root": false, "enabled": false, "paths": { "source-path": [], From 8b43c5e733117d528a164f45a6112b6d76a53e42 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 4 May 2022 12:16:19 +0200 Subject: [PATCH 117/194] add a tab in nuke project settings for gizmos --- openpype/hosts/nuke/startup/menu.py | 1 - .../defaults/project_settings/nuke.json | 22 ++++++++++ .../projects_schema/schema_project_nuke.json | 4 ++ .../schemas/schema_nuke_scriptsgizmo.json | 42 +++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 49edb22a89..eea2d940f8 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -31,7 +31,6 @@ nuke.addFilenameFilter(dirmap_file_name_filter) log.info('Automatic syncing of write file knob to script version') - def add_scripts_menu(): try: from scriptsmenu import launchfornuke diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index dc8ffcebff..a10b88464c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -290,5 +290,27 @@ } ] }, + "gizmo": [ + { + "toolbar_menu_name": "FixStudio", + "toolbar_icon_path": "{QUAD_PLUGIN_PATH}/nuke/icons/fixstudio.png", + "gizmo_path": ["{QUAD_PLUGIN_PATH}/nuke/gizmos"], + "gizmo_definition": [ + { + "type": "menu", + "title": "3D", + "items": [ + { + "type": "action", + "command": "nuke.createNode('Camera_Smoother')", + "sourcetype": "python", + "title": "Camera_Smoother" + } + + ] + } + ] + } + ], "filters": {} } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json index 1ae4efd8ea..03d67a57ba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_nuke.json @@ -83,6 +83,10 @@ "type": "schema", "name": "schema_scriptsmenu" }, + { + "type": "schema", + "name": "schema_nuke_scriptsgizmo" + }, { "type": "dict", "collapsible": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json new file mode 100644 index 0000000000..c1e67842ce --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json @@ -0,0 +1,42 @@ +{ + "type": "list", + "key": "gizmo", + "label": "Gizmo Menu", + "is_group": true, + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "text", + "key": "toolbar_menu_name", + "label": "Toolbar Menu Name" + }, + { + "type": "path", + "key": "toolbar_icon_path", + "label": "Toolbar Icon Path", + "multipath": false + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Absolute path to gizmo folders." + }, + { + "type": "path", + "key": "gizmo_path", + "label": "Gizmo Path", + "multipath": true + }, + { + "type": "raw-json", + "key": "gizmo_definition", + "label": "Gizmo definition", + "is_list": true + } + ] + } +} \ No newline at end of file From 25518a1c42ff9958f9bb9763b0792dd28e8cc691 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 4 May 2022 17:01:32 +0200 Subject: [PATCH 118/194] generate toolbar menu from openpype project settings --- openpype/hosts/nuke/api/lib.py | 48 +++++++++++++++ openpype/hosts/nuke/startup/menu.py | 59 ++++++++++++++++++- .../defaults/project_settings/nuke.json | 9 ++- 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index f40425eefc..a1ac50ae1a 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -30,6 +30,8 @@ from openpype.pipeline import ( legacy_io, ) +from . import gizmo_menu + from .workio import ( save_file, open_file @@ -2498,6 +2500,52 @@ def recreate_instance(origin_node, avalon_data=None): return new_node +def find_scripts_gizmo(title, parent): + """ + Check if the menu exists with the given title in the parent + + Args: + title (str): the title name of the scripts menu + + parent (QtWidgets.QMenuBar): the menubar to check + + Returns: + QtWidgets.QMenu or None + + """ + + menu = None + search = [i for i in parent.items() if + isinstance(i, gizmo_menu.GizmoMenu) + and i.title() == title] + + if search: + assert len(search) < 2, ("Multiple instances of menu '{}' " + "in toolbar".format(title)) + menu = search[0] + + return menu + + +def gizmo_creation(title="Gizmos", parent=None, objectName=None, icon=None): + try: + toolbar = find_scripts_gizmo(title, parent) + if not toolbar: + log.info("Attempting to build toolbar...") + object_name = objectName or title.lower() + toolbar = gizmo_menu.GizmoMenu( + title=title, + parent=parent, + objectName=object_name, + icon=icon + ) + except Exception as e: + log.error(e) + return + + return toolbar + + class NukeDirmap(HostDirmap): def __init__(self, host_name, project_settings, sync_module, file_name): """ diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index eea2d940f8..0f587fc62a 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -2,7 +2,7 @@ import nuke import os from openpype.api import Logger -from openpype.pipeline import install_host +from openpype.settings import get_project_settings from openpype.hosts.nuke import api from openpype.hosts.nuke.api.lib import ( on_script_load, @@ -31,6 +31,7 @@ nuke.addFilenameFilter(dirmap_file_name_filter) log.info('Automatic syncing of write file knob to script version') + def add_scripts_menu(): try: from scriptsmenu import launchfornuke @@ -58,3 +59,59 @@ def add_scripts_menu(): add_scripts_menu() + + +def add_scripts_gizmo(): + try: + from openpype.hosts.nuke.api import lib + except ImportError: + log.warning( + "Skipping studio.gizmo install, because " + "'scriptsgizmo' module seems unavailable." + ) + return + + for gizmo in project_settings["nuke"]["gizmo"]: + config = gizmo["gizmo_definition"] + toolbar_name = gizmo["toolbar_menu_name"] + gizmo_path = gizmo["gizmo_path"] + icon = gizmo['toolbar_icon_path'] + + if not any(gizmo_path): + log.warning("Skipping studio gizmo, no gizmo path found.") + return + + if not config: + log.warning("Skipping studio gizmo, no definition found.") + return + + try: + icon = icon.format(**os.environ) + except KeyError as e: + log.warning(f"This environment variable doesn't exist: {e}") + + for gizmo in gizmo_path: + try: + gizmo = gizmo.format(**os.environ) + gizmo_path.append(gizmo) + gizmo_path.pop(0) + except KeyError as e: + log.warning(f"This environment variable doesn't exist: {e}") + + nuke_toolbar = nuke.menu("Nodes") + toolbar = nuke_toolbar.addMenu(toolbar_name, icon=icon) + + # run the launcher for Nuke toolbar + studio_menu = lib.gizmo_creation( + title=toolbar_name, + parent=toolbar, + objectName=toolbar_name.lower().replace(" ", "_"), + icon=icon + ) + + # apply configuration + studio_menu.add_gizmo_path(gizmo_path) + studio_menu.build_from_configuration(toolbar, config) + + +add_scripts_gizmo() diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index a10b88464c..48bbbf0dcc 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -293,8 +293,8 @@ "gizmo": [ { "toolbar_menu_name": "FixStudio", - "toolbar_icon_path": "{QUAD_PLUGIN_PATH}/nuke/icons/fixstudio.png", - "gizmo_path": ["{QUAD_PLUGIN_PATH}/nuke/gizmos"], + "toolbar_icon_path": "openpype/modules/quad/nuke/icons/fixstudio.png", + "gizmo_path": ["openpype/modules/quad/nuke/gizmos/3D"], "gizmo_definition": [ { "type": "menu", @@ -302,11 +302,10 @@ "items": [ { "type": "action", - "command": "nuke.createNode('Camera_Smoother')", "sourcetype": "python", - "title": "Camera_Smoother" + "title": "Camera Smoother", + "command": "nuke.createNodes('Camera_Smoother)" } - ] } ] From af259d215e08d54cca7dd01754a03f8b0863aefe Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 15:12:16 +0200 Subject: [PATCH 119/194] refactor default gizmo --- openpype/settings/defaults/project_settings/nuke.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 48bbbf0dcc..d9b443c958 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -293,8 +293,8 @@ "gizmo": [ { "toolbar_menu_name": "FixStudio", - "toolbar_icon_path": "openpype/modules/quad/nuke/icons/fixstudio.png", - "gizmo_path": ["openpype/modules/quad/nuke/gizmos/3D"], + "toolbar_icon_path": "path/to/nuke/icon.png", + "gizmo_path": ["path/to/nuke/gizmo"], "gizmo_definition": [ { "type": "menu", @@ -304,7 +304,7 @@ "type": "action", "sourcetype": "python", "title": "Camera Smoother", - "command": "nuke.createNodes('Camera_Smoother)" + "command": "nuke.createNode('Camera_Smoother')" } ] } From 5bc741d993b67a3e9ebfd74b5cc711caa56e06fb Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 9 May 2022 15:13:50 +0200 Subject: [PATCH 120/194] add gizmo_menu module --- openpype/hosts/nuke/api/gizmo_menu.py | 77 +++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 openpype/hosts/nuke/api/gizmo_menu.py diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py new file mode 100644 index 0000000000..56532ed1dc --- /dev/null +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -0,0 +1,77 @@ +import os +import logging +import nuke + +log = logging.getLogger(__name__) + + +class GizmoMenu(): + def __init__(self, *args, **kwargs): + self._script_actions = [] + + def build_from_configuration(self, parent, configuration): + for item in configuration: + assert isinstance(item, dict), "Configuration is wrong!" + + # skip items which have no `type` key + item_type = item.get('type', None) + if not item_type: + log.warning("Missing 'type' from configuration item") + continue + + if item_type == "action": + # filter out `type` from the item dict + config = {key: value for key, value in + item.items() if key != "type"} + + command = config['command'] + + if command.find('{pipe_path}') > -1: + command = command.format( + pipe_path=os.environ['QUAD_PLUGIN_PATH'] + ) + + icon = config.get('icon', None) + if icon: + try: + icon = icon.format(**os.environ) + except KeyError as e: + log.warning(f"This environment variable doesn't exist: {e}'") + + hotkey = config.get('hotkey', None) + + parent.addCommand( + config['title'], + command=command, + icon=icon, + shortcut=hotkey + ) + + # add separator + # Special behavior for separators + if item_type == "separator": + parent.addSeparator() + + # add submenu + # items should hold a collection of submenu items (dict) + elif item_type == "menu": + assert "items" in item, "Menu is missing 'items' key" + + icon = item.get('icon', None) + if icon: + try: + icon = icon.format(**os.environ) + except KeyError as e: + log.warning(f"This environment variable doesn't exist: {e}'") + menu = parent.addMenu(item['title'], icon=icon) + self.build_from_configuration(menu, item["items"]) + + def add_gizmo_path(self, gizmo_paths): + for gizmo_path in gizmo_paths: + if os.path.isdir(gizmo_path): + for folder in os.listdir(gizmo_path): + if os.path.isdir(os.path.join(gizmo_path, folder)): + nuke.pluginAddPath(os.path.join(gizmo_path, folder)) + nuke.pluginAddPath(gizmo_path) + else: + log.warning(f"This path doesn't exist: {gizmo_path}") From 94356faa9c16f359dca2e94a726e79f16253e1d3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 11 May 2022 11:46:57 +0200 Subject: [PATCH 121/194] fix install_host import --- openpype/hosts/nuke/startup/menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 0f587fc62a..88c727aaa6 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -2,7 +2,7 @@ import nuke import os from openpype.api import Logger -from openpype.settings import get_project_settings +from openpype.pipeline import install_host from openpype.hosts.nuke import api from openpype.hosts.nuke.api.lib import ( on_script_load, From 0696d505c2cf9eb53c10afe2c2ffa6b73f3cbe7b Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 12 May 2022 11:12:08 +0200 Subject: [PATCH 122/194] set the default gizmo to a sticky note --- .../settings/defaults/project_settings/nuke.json | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index d9b443c958..06679ac314 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -292,21 +292,15 @@ }, "gizmo": [ { - "toolbar_menu_name": "FixStudio", + "toolbar_menu_name": "OpenPype Gizmo", "toolbar_icon_path": "path/to/nuke/icon.png", "gizmo_path": ["path/to/nuke/gizmo"], "gizmo_definition": [ { - "type": "menu", - "title": "3D", - "items": [ - { - "type": "action", - "sourcetype": "python", - "title": "Camera Smoother", - "command": "nuke.createNode('Camera_Smoother')" - } - ] + "type": "action", + "sourcetype": "python", + "title": "Gizmo Note", + "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')" } ] } From bd7243f8aa1dd64d3bad4ef822eb24e4ee70e4d4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 12 May 2022 11:20:55 +0200 Subject: [PATCH 123/194] add the docs for the gizmo menu --- website/docs/admin_hosts_nuke.md | 4 ++++ website/docs/assets/nuke-admin_gizmomenu.png | Bin 0 -> 34803 bytes 2 files changed, 4 insertions(+) create mode 100644 website/docs/assets/nuke-admin_gizmomenu.png diff --git a/website/docs/admin_hosts_nuke.md b/website/docs/admin_hosts_nuke.md index 46f596a2dc..bab63223ce 100644 --- a/website/docs/admin_hosts_nuke.md +++ b/website/docs/admin_hosts_nuke.md @@ -12,3 +12,7 @@ You can add your custom tools menu into Nuke by extending definitions in **Nuke This is still work in progress. Menu definition will be handled more friendly with widgets and not raw json. ::: + +## Gizmo Menu +You can add your custom toolbar menu into Nuke by setting your gizmo path and extending definitions in **Nuke -> Gizmo Menu**. +![Custom menu definition](assets/nuke-admin_gizmomenu.png) diff --git a/website/docs/assets/nuke-admin_gizmomenu.png b/website/docs/assets/nuke-admin_gizmomenu.png new file mode 100644 index 0000000000000000000000000000000000000000..81e63b20418896173bf5f6417ce2515780e2bcdf GIT binary patch literal 34803 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 KXW7E9J^v4^Goq*f literal 0 HcmV?d00001 From 08e04911b3c100ca1dba8d1107a33cf689c98e79 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Thu, 12 May 2022 15:55:11 +0200 Subject: [PATCH 124/194] fix string format for python2 --- openpype/hosts/nuke/api/gizmo_menu.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py index 56532ed1dc..c1132792d0 100644 --- a/openpype/hosts/nuke/api/gizmo_menu.py +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -36,7 +36,8 @@ class GizmoMenu(): try: icon = icon.format(**os.environ) except KeyError as e: - log.warning(f"This environment variable doesn't exist: {e}'") + log.warning("This environment variable doesn't exist: " + "{}".format(e)) hotkey = config.get('hotkey', None) @@ -62,7 +63,8 @@ class GizmoMenu(): try: icon = icon.format(**os.environ) except KeyError as e: - log.warning(f"This environment variable doesn't exist: {e}'") + log.warning("This environment variable doesn't exist: " + "{}".format(e)) menu = parent.addMenu(item['title'], icon=icon) self.build_from_configuration(menu, item["items"]) @@ -74,4 +76,4 @@ class GizmoMenu(): nuke.pluginAddPath(os.path.join(gizmo_path, folder)) nuke.pluginAddPath(gizmo_path) else: - log.warning(f"This path doesn't exist: {gizmo_path}") + log.warning("This path doesn't exist: {}".format(gizmo_path)) From eb590602c329b90d8b1e40aeda4ae04287e01a06 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 1 Jun 2022 17:31:19 +0200 Subject: [PATCH 125/194] make it works on nuke 12 --- openpype/hosts/nuke/api/gizmo_menu.py | 2 +- openpype/hosts/nuke/startup/menu.py | 11 +++++++++-- openpype/settings/defaults/project_settings/nuke.json | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py index c1132792d0..a541fd3ab1 100644 --- a/openpype/hosts/nuke/api/gizmo_menu.py +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -24,7 +24,7 @@ class GizmoMenu(): config = {key: value for key, value in item.items() if key != "type"} - command = config['command'] + command = str(config['command']) if command.find('{pipe_path}') > -1: command = command.format( diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 88c727aaa6..6c076fc87b 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -71,6 +71,9 @@ def add_scripts_gizmo(): ) return + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + for gizmo in project_settings["nuke"]["gizmo"]: config = gizmo["gizmo_definition"] toolbar_name = gizmo["toolbar_menu_name"] @@ -88,7 +91,9 @@ def add_scripts_gizmo(): try: icon = icon.format(**os.environ) except KeyError as e: - log.warning(f"This environment variable doesn't exist: {e}") + log.warning( + "This environment variable doesn't exist: {}".format(e) + ) for gizmo in gizmo_path: try: @@ -96,7 +101,9 @@ def add_scripts_gizmo(): gizmo_path.append(gizmo) gizmo_path.pop(0) except KeyError as e: - log.warning(f"This environment variable doesn't exist: {e}") + log.warning( + "This environment variable doesn't exist: {}".format(e) + ) nuke_toolbar = nuke.menu("Nodes") toolbar = nuke_toolbar.addMenu(toolbar_name, icon=icon) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 06679ac314..6c6454de36 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -306,4 +306,4 @@ } ], "filters": {} -} \ No newline at end of file +} From 8f9c08549292a58b7beccf3ae8a9477b2aac1020 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 1 Jun 2022 20:40:28 +0200 Subject: [PATCH 126/194] beter loop check --- openpype/hosts/nuke/startup/menu.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 6c076fc87b..715bab8ea5 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -95,15 +95,21 @@ def add_scripts_gizmo(): "This environment variable doesn't exist: {}".format(e) ) + existing_gizmo_path = [] for gizmo in gizmo_path: try: gizmo = gizmo.format(**os.environ) - gizmo_path.append(gizmo) - gizmo_path.pop(0) except KeyError as e: log.warning( "This environment variable doesn't exist: {}".format(e) ) + continue + if not os.path.exists(gizmo): + log.warning( + "The source of gizmo `{}` does not exists".format(gizmo) + ) + continue + existing_gizmo_path.append(gizmo) nuke_toolbar = nuke.menu("Nodes") toolbar = nuke_toolbar.addMenu(toolbar_name, icon=icon) @@ -117,7 +123,7 @@ def add_scripts_gizmo(): ) # apply configuration - studio_menu.add_gizmo_path(gizmo_path) + studio_menu.add_gizmo_path(existing_gizmo_path) studio_menu.build_from_configuration(toolbar, config) From 0fcfdf7fa8250d400a7da772be12972b59c667fd Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 1 Jun 2022 20:44:10 +0200 Subject: [PATCH 127/194] remove studio operation --- openpype/hosts/nuke/api/gizmo_menu.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py index a541fd3ab1..dd04f4a42e 100644 --- a/openpype/hosts/nuke/api/gizmo_menu.py +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -26,11 +26,6 @@ class GizmoMenu(): command = str(config['command']) - if command.find('{pipe_path}') > -1: - command = command.format( - pipe_path=os.environ['QUAD_PLUGIN_PATH'] - ) - icon = config.get('icon', None) if icon: try: From 9fe4b635174060697a9ea8a9e33f47394a34d9e9 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 128/194] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 69a388de1319eb49de84a0f6d846631623fc5a7d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 129/194] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a42f889e85..303cd052bb 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From 108597f9b1e139f31e6b0f20568866cb2971020a Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 130/194] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 199aba87727d7a2417d7f8122dd34f6e4160b467 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 131/194] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_entities (list): Asset entities for which want to find data + + Returns: + (dict): collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: return # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From bd884262b0c001715432f28ec1cae6feeeabfed1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 132/194] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From 60cc108251db884a04cef1d2ea29a558a7750b8c Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 133/194] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From aaa1f13f9d0ae038f70eb2cdc21cba56f92b97dd Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 134/194] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From c2aca3422c8c2e29a169f9550e7e1719733f7ec4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 135/194] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 95d3686889470a8ad6d677b949a86cab094e47ea Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 136/194] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From e29d4e5699e6dace616933317c57fcc9bc43c878 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 137/194] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From 28518eeb21f2a9ef56c32c0009ce09aecf871a86 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 138/194] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From b65a1d4e79e3fa2ff4ca11392f9ccbce68a19a78 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 139/194] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From b095249fb859c9845d00efb8d69bd515867c6e94 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 140/194] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From 79c9dc94528ff8f3ae216f106b2225ae790fb044 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 141/194] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From 7afa319b25ce94cb3cb64b1c050ad23eeb1cd873 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 12:54:27 +0200 Subject: [PATCH 142/194] add subdir 'bin' when oiio path is prepared --- openpype/lib/vendor_bin_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/lib/vendor_bin_utils.py b/openpype/lib/vendor_bin_utils.py index 23e28ea304..e5ab2872a0 100644 --- a/openpype/lib/vendor_bin_utils.py +++ b/openpype/lib/vendor_bin_utils.py @@ -116,7 +116,10 @@ def get_oiio_tools_path(tool="oiiotool"): tool (string): Tool name (oiiotool, maketx, ...). Default is "oiiotool". """ + oiio_dir = get_vendor_bin_path("oiio") + if platform.system().lower() == "linux": + oiio_dir = os.path.join(oiio_dir, "bin") return find_executable(os.path.join(oiio_dir, tool)) From 0034d7495cebc18f0cee2466d1109d69cf52a234 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Jun 2022 12:14:23 +0200 Subject: [PATCH 143/194] Hiero: add support for task tags and collecting tags in general --- openpype/hosts/hiero/api/__init__.py | 2 ++ openpype/hosts/hiero/api/lib.py | 25 +++++++++++++++++++ .../collect_tag_tasks.py | 6 ++--- .../plugins/publish/precollect_instances.py | 5 +++- 4 files changed, 34 insertions(+), 4 deletions(-) rename openpype/hosts/hiero/plugins/{publish_old_workflow => publish}/collect_tag_tasks.py (91%) diff --git a/openpype/hosts/hiero/api/__init__.py b/openpype/hosts/hiero/api/__init__.py index fc2d017f04..781f846bbe 100644 --- a/openpype/hosts/hiero/api/__init__.py +++ b/openpype/hosts/hiero/api/__init__.py @@ -29,6 +29,7 @@ from .lib import ( get_current_sequence, get_timeline_selection, get_current_track, + get_track_item_tags, get_track_item_pype_tag, set_track_item_pype_tag, get_track_item_pype_data, @@ -83,6 +84,7 @@ __all__ = [ "get_current_sequence", "get_timeline_selection", "get_current_track", + "get_track_item_tags", "get_track_item_pype_tag", "set_track_item_pype_tag", "get_track_item_pype_data", diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index 761a36bd0f..06dfd2f2ee 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -274,6 +274,31 @@ def _validate_all_atrributes( ]) +def get_track_item_tags(track_item): + """ + Get track item tags excluded openpype tag + + Attributes: + trackItem (hiero.core.TrackItem): hiero object + + Returns: + hiero.core.Tag: hierarchy, orig clip attributes + """ + returning_tag_data = [] + # get all tags from track item + _tags = track_item.tags() + if not _tags: + return [] + + # collect all tags which are not openpype tag + returning_tag_data.extend( + tag for tag in _tags + if tag.name() != self.pype_tag_name + ) + + return returning_tag_data + + def get_track_item_pype_tag(track_item): """ Get pype track item tag created by creator or loader plugin. diff --git a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_tasks.py b/openpype/hosts/hiero/plugins/publish/collect_tag_tasks.py similarity index 91% rename from openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_tasks.py rename to openpype/hosts/hiero/plugins/publish/collect_tag_tasks.py index 70891d5b45..27968060e1 100644 --- a/openpype/hosts/hiero/plugins/publish_old_workflow/collect_tag_tasks.py +++ b/openpype/hosts/hiero/plugins/publish/collect_tag_tasks.py @@ -4,16 +4,16 @@ from pyblish import api class CollectClipTagTasks(api.InstancePlugin): """Collect Tags from selected track items.""" - order = api.CollectorOrder + order = api.CollectorOrder - 0.077 label = "Collect Tag Tasks" hosts = ["hiero"] - families = ['clip'] + families = ["shot"] def process(self, instance): # gets tags tags = instance.data["tags"] - tasks = dict() + tasks = {} for tag in tags: t_metadata = dict(tag.metadata()) t_family = t_metadata.get("tag.family", "") diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index e54d050f0d..b891a37d9d 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -106,7 +106,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # clip's effect "clipEffectItems": subtracks, - "clipAnnotations": annotations + "clipAnnotations": annotations, + + # add all additional tags + "tags": phiero.get_track_item_tags(track_item) }) # otio clip data From 3464a7c5d7bceb1174237aeecc25e9d12c178638 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Jun 2022 13:48:41 +0200 Subject: [PATCH 144/194] global: hierarchy publishing only to active instances filter --- .../publish/integrate_hierarchy_ftrack.py | 47 +++++++++++-- openpype/plugins/publish/collect_hierarchy.py | 4 -- .../publish/extract_hierarchy_avalon.py | 70 +++++++++---------- 3 files changed, 76 insertions(+), 45 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index cf90c11b65..73398941eb 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -2,7 +2,7 @@ import sys import collections import six import pyblish.api - +from copy import deepcopy from openpype.pipeline import legacy_io # Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` @@ -72,7 +72,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): if "hierarchyContext" not in self.context.data: return - hierarchy_context = self.context.data["hierarchyContext"] + hierarchy_context = self._get_active_assets(context) + self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) self.session = self.context.data["ftrackSession"] project_name = self.context.data["projectEntity"]["name"] @@ -86,15 +87,13 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.ft_project = None - input_data = hierarchy_context - # disable termporarily ftrack project's autosyncing if auto_sync_state: self.auto_sync_off(project) try: # import ftrack hierarchy - self.import_to_ftrack(input_data) + self.import_to_ftrack(hierarchy_context) except Exception: raise finally: @@ -355,3 +354,41 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.session.rollback() self.session._configure_locations() six.reraise(tp, value, tb) + + def _get_active_assets(self, context): + """ Returns only asset dictionary. + Usually the last part of deep dictionary which + is not having any children + """ + def get_pure_hierarchy_data(input_dict): + input_dict_copy = deepcopy(input_dict) + for key in input_dict.keys(): + self.log.debug("__ key: {}".format(key)) + # check if child key is available + if input_dict[key].get("childs"): + # loop deeper + input_dict_copy[ + key]["childs"] = get_pure_hierarchy_data( + input_dict[key]["childs"]) + elif key not in active_assets: + input_dict_copy.pop(key, None) + return input_dict_copy + + hierarchy_context = context.data["hierarchyContext"] + + active_assets = [] + # filter only the active publishing insatnces + for instance in context: + if instance.data.get("publish") is False: + continue + + if not instance.data.get("asset"): + continue + + active_assets.append(instance.data["asset"]) + + # remove duplicity in list + active_assets = list(set(active_assets)) + self.log.debug("__ active_assets: {}".format(active_assets)) + + return get_pure_hierarchy_data(hierarchy_context) diff --git a/openpype/plugins/publish/collect_hierarchy.py b/openpype/plugins/publish/collect_hierarchy.py index 8398a2815a..91d5162d62 100644 --- a/openpype/plugins/publish/collect_hierarchy.py +++ b/openpype/plugins/publish/collect_hierarchy.py @@ -33,10 +33,6 @@ class CollectHierarchy(pyblish.api.ContextPlugin): family = instance.data["family"] families = instance.data["families"] - # filter out all unepropriate instances - if not instance.data["publish"]: - continue - # exclude other families then self.families with intersection if not set(self.families).intersection(set(families + [family])): continue diff --git a/openpype/plugins/publish/extract_hierarchy_avalon.py b/openpype/plugins/publish/extract_hierarchy_avalon.py index 2f528d4469..1f7ce839ed 100644 --- a/openpype/plugins/publish/extract_hierarchy_avalon.py +++ b/openpype/plugins/publish/extract_hierarchy_avalon.py @@ -1,7 +1,5 @@ from copy import deepcopy - import pyblish.api - from openpype.pipeline import legacy_io @@ -17,33 +15,16 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): if "hierarchyContext" not in context.data: self.log.info("skipping IntegrateHierarchyToAvalon") return - hierarchy_context = deepcopy(context.data["hierarchyContext"]) if not legacy_io.Session: legacy_io.install() - active_assets = [] - # filter only the active publishing insatnces - for instance in context: - if instance.data.get("publish") is False: - continue - - if not instance.data.get("asset"): - continue - - active_assets.append(instance.data["asset"]) - - # remove duplicity in list - self.active_assets = list(set(active_assets)) - self.log.debug("__ self.active_assets: {}".format(self.active_assets)) - - hierarchy_context = self._get_assets(hierarchy_context) - + hierarchy_context = self._get_active_assets(context) self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) - input_data = context.data["hierarchyContext"] = hierarchy_context self.project = None - self.import_to_avalon(input_data) + self.import_to_avalon(hierarchy_context) + def import_to_avalon(self, input_data, parent=None): for name in input_data: @@ -183,23 +164,40 @@ class ExtractHierarchyToAvalon(pyblish.api.ContextPlugin): return legacy_io.find_one({"_id": entity_id}) - def _get_assets(self, input_dict): + def _get_active_assets(self, context): """ Returns only asset dictionary. Usually the last part of deep dictionary which is not having any children """ - input_dict_copy = deepcopy(input_dict) - - for key in input_dict.keys(): - self.log.debug("__ key: {}".format(key)) - # check if child key is available - if input_dict[key].get("childs"): - # loop deeper - input_dict_copy[key]["childs"] = self._get_assets( - input_dict[key]["childs"]) - else: - # filter out unwanted assets - if key not in self.active_assets: + def get_pure_hierarchy_data(input_dict): + input_dict_copy = deepcopy(input_dict) + for key in input_dict.keys(): + self.log.debug("__ key: {}".format(key)) + # check if child key is available + if input_dict[key].get("childs"): + # loop deeper + input_dict_copy[ + key]["childs"] = get_pure_hierarchy_data( + input_dict[key]["childs"]) + elif key not in active_assets: input_dict_copy.pop(key, None) + return input_dict_copy - return input_dict_copy + hierarchy_context = context.data["hierarchyContext"] + + active_assets = [] + # filter only the active publishing insatnces + for instance in context: + if instance.data.get("publish") is False: + continue + + if not instance.data.get("asset"): + continue + + active_assets.append(instance.data["asset"]) + + # remove duplicity in list + active_assets = list(set(active_assets)) + self.log.debug("__ active_assets: {}".format(active_assets)) + + return get_pure_hierarchy_data(hierarchy_context) From a9fdcd80aaee0db6dddc0e777e9dd021459f04c2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 3 Jun 2022 14:44:11 +0200 Subject: [PATCH 145/194] OP-3231 - return only active projects in webpublisher ProjectsEndpoing --- .../webserver_service/webpublish_routes.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py index e82ba7f2b8..70324fc39c 100644 --- a/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py +++ b/openpype/hosts/webpublisher/webserver_service/webpublish_routes.py @@ -71,16 +71,12 @@ class ProjectsEndpoint(_RestApiEndpoint): """Returns list of dict with project info (id, name).""" async def get(self) -> Response: output = [] - for project_name in self.dbcon.database.collection_names(): - project_doc = self.dbcon.database[project_name].find_one({ - "type": "project" - }) - if project_doc: - ret_val = { - "id": project_doc["_id"], - "name": project_doc["name"] - } - output.append(ret_val) + for project_doc in self.dbcon.projects(): + ret_val = { + "id": project_doc["_id"], + "name": project_doc["name"] + } + output.append(ret_val) return Response( status=200, body=self.resource.encode(output), From 98504a205210471242f0c955d3f549561ec392df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:45:05 +0200 Subject: [PATCH 146/194] implemented action that can tranfer values of 1 hierarchical attribute to another --- .../action_translate_hierarchical_values.py | 331 ++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py diff --git a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py new file mode 100644 index 0000000000..fd10005fad --- /dev/null +++ b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py @@ -0,0 +1,331 @@ +import copy +import json +import collections + +import ftrack_api + +from openpype_modules.ftrack.lib import ( + ServerAction, + statics_icon, +) +from openpype_modules.ftrack.lib.avalon_sync import create_chunks + + +class TranslateHierarchicalValues(ServerAction): + """Transfer values across hierarhcical attributes. + + Aalso gives ability to convert types meanwhile. That is limited to + conversions between numbers and strings + - int <-> float + - in, float -> string + """ + + identifier = "translate.hierarchical.values" + label = "OpenPype Admin" + variant = "- Translate values between 2 custom attributes" + description = ( + "Move values from a hierarchical attribute to" + " second hierarchical attribute." + ) + icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") + + all_project_entities_query = ( + "select id, name, parent_id, link" + " from TypedContext where project_id is \"{}\"" + ) + cust_attr_query = ( + "select value, entity_id from CustomAttributeValue" + " where entity_id in ({}) and configuration_id is \"{}\"" + ) + settings_key = "clean_hierarchical_attr" + + def discover(self, session, entities, event): + """Show anywhere.""" + + return self.valid_roles(session, entities, event) + + def _selection_interface(self, session, event_values=None): + title = "Translate hierarchical values" + + attr_confs = session.query( + ( + "select id, key from CustomAttributeConfiguration" + " where is_hierarchical is true" + ) + ).all() + attr_items = [] + for attr_conf in attr_confs: + attr_items.append({ + "value": attr_conf["id"], + "label": attr_conf["key"] + }) + + if len(attr_items) < 2: + return { + "title": title, + "items": [{ + "type": "label", + "value": ( + "Didn't found custom attributes" + " that can be translated." + ) + }] + } + + attr_items = sorted(attr_items, key=lambda item: item["label"]) + items = [] + item_splitter = {"type": "label", "value": "---"} + items.append({ + "type": "label", + "value": ( + "

Please select source and destination" + " Custom attribute

" + ) + }) + items.append({ + "type": "label", + "value": ( + "WARNING: This will take affect for all projects!" + ) + }) + if event_values: + items.append({ + "type": "label", + "value": ( + "Note: Please select 2 different custom attributes." + ) + }) + + items.append(item_splitter) + + src_item = { + "type": "enumerator", + "label": "Source", + "name": "src_attr_id", + "data": copy.deepcopy(attr_items) + } + dst_item = { + "type": "enumerator", + "label": "Destination", + "name": "dst_attr_id", + "data": copy.deepcopy(attr_items) + } + delete_item = { + "type": "boolean", + "name": "delete_dst_attr_first", + "label": "Delete first", + "value": False + } + if event_values: + src_item["value"] = event_values["src_attr_id"] + dst_item["value"] = event_values["dst_attr_id"] + delete_item["value"] = event_values["delete_dst_attr_first"] + + items.append(src_item) + items.append(dst_item) + items.append(item_splitter) + items.append({ + "type": "label", + "value": ( + "WARNING: All values from destination" + " Custom Attribute will be removed if this is enabled." + ) + }) + items.append(delete_item) + + return { + "title": title, + "items": items + } + + def interface(self, session, entities, event): + if event["data"].get("values", {}): + return None + + return self._selection_interface(session) + + def launch(self, session, entities, event): + values = event["data"].get("values", {}) + if not values: + return None + src_attr_id = values["src_attr_id"] + dst_attr_id = values["dst_attr_id"] + delete_dst_values = values["delete_dst_attr_first"] + + if not src_attr_id or not dst_attr_id: + return { + "success": True, + "message": "Nothing to do" + } + + if src_attr_id == dst_attr_id: + return self._selection_interface(session, values) + + # Query custom attrbutes + src_conf = session.query(( + "select id from CustomAttributeConfiguration where id is {}" + ).format(src_attr_id)).one() + dst_conf = session.query(( + "select id from CustomAttributeConfiguration where id is {}" + ).format(dst_attr_id)).one() + src_type_name = src_conf["type"]["name"] + dst_type_name = dst_conf["type"]["name"] + # Limit conversion to + # - same type -> same type (there is no need to do conversion) + # - number -> number (int to float and back) + # - number -> str (any number can be converted to str) + src_type = None + dst_type = None + if src_type_name == "number" or src_type_name != dst_type_name: + src_type = self._get_attr_type(dst_conf) + dst_type = self._get_attr_type(dst_conf) + valid = False + # Can convert numbers + if src_type in (int, float) and dst_type in (int, float): + valid = True + # Can convert numbers to string + elif dst_type is str: + valid = True + + if not valid: + return { + "message": ( + "Don't know how to properly convert" + " custom attribute types {} > {}" + ).format(src_type_name, dst_type_name), + "success": False + } + + # Query source values + src_attr_values = session.query( + ( + "select value, entity_id" + " from CustomAttributeValue" + " where configuration_id is {}" + ).format(src_attr_id) + ).all() + + value_by_id = {} + failed_entity_ids = [] + for attr_value in src_attr_values: + entity_id = attr_value["entity_id"] + value = attr_value["value"] + if value is not None: + try: + if dst_type is not None: + value = dst_type(value) + value_by_id[entity_id] = value + except Exception: + failed_entity_ids.append(entity_id) + + if failed_entity_ids: + return { + "success": False, + "message": ( + "Couldn't convert some values to destination attribute" + ) + } + + + # Delete destination custom attributes first + if delete_dst_values: + self.log.info("Deleting destination custom attribute values first") + self._delete_custom_attribute_values(session, dst_attr_id) + + self._apply_values(session, value_by_id, dst_attr_id) + return True + + def _delete_custom_attribute_values(self, session, dst_attr_id): + dst_attr_values = session.query( + ( + "select configuration_id, entity_id" + " from CustomAttributeValue" + " where configuration_id is {}" + ).format(dst_attr_id) + ).all() + delete_operations = [] + for attr_value in dst_attr_values: + entity_id = attr_value["entity_id"] + configuration_id = attr_value["configuration_id"] + entity_key = collections.OrderedDict(( + ("configuration_id", configuration_id), + ("entity_id", entity_id) + )) + delete_operations.append( + ftrack_api.operation.DeleteEntityOperation( + "CustomAttributeValue", + entity_key + ) + ) + + if not delete_operations: + return + + for chunk in create_chunks(delete_operations, 500): + for operation in chunk: + session.recorded_operations.push(operation) + session.commit() + + def _apply_values(self, session, value_by_id, dst_attr_id): + dst_attr_values = session.query( + ( + "select configuration_id, entity_id" + " from CustomAttributeValue" + " where configuration_id is {}" + ).format(dst_attr_id) + ).all() + + dst_entity_ids_with_value = { + item["entity_id"] + for item in dst_attr_values + } + operations = [] + for entity_id, value in value_by_id.items(): + entity_key = collections.OrderedDict(( + ("configuration_id", dst_attr_id), + ("entity_id", entity_id) + )) + if entity_id in dst_entity_ids_with_value: + operations.append( + ftrack_api.operation.UpdateEntityOperation( + "CustomAttributeValue", + entity_key, + "value", + ftrack_api.symbol.NOT_SET, + value + ) + ) + else: + operations.append( + ftrack_api.operation.CreateEntityOperation( + "CustomAttributeValue", + entity_key, + {"value": value} + ) + ) + + if not operations: + return + + for chunk in create_chunks(operations, 500): + for operation in chunk: + session.recorded_operations.push(operation) + session.commit() + + def _get_attr_type(self, conf_def): + type_name = conf_def["type"]["name"] + if type_name == "text": + return str + + if type_name == "number": + config = json.loads(conf_def["config"]) + if config["isdecimal"]: + return float + return int + return None + + +def register(session): + '''Register plugin. Called when used as an plugin.''' + + TranslateHierarchicalValues(session).register() From 8e7358a33b9da4a97681a1dae4119e30b238a24a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:51:21 +0200 Subject: [PATCH 147/194] changed translate to transfer --- .../action_translate_hierarchical_values.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py index fd10005fad..8cc6fa3a57 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py @@ -11,7 +11,7 @@ from openpype_modules.ftrack.lib import ( from openpype_modules.ftrack.lib.avalon_sync import create_chunks -class TranslateHierarchicalValues(ServerAction): +class TransferHierarchicalValues(ServerAction): """Transfer values across hierarhcical attributes. Aalso gives ability to convert types meanwhile. That is limited to @@ -20,9 +20,9 @@ class TranslateHierarchicalValues(ServerAction): - in, float -> string """ - identifier = "translate.hierarchical.values" + identifier = "transfer.hierarchical.values" label = "OpenPype Admin" - variant = "- Translate values between 2 custom attributes" + variant = "- Transfer values between 2 custom attributes" description = ( "Move values from a hierarchical attribute to" " second hierarchical attribute." @@ -37,7 +37,7 @@ class TranslateHierarchicalValues(ServerAction): "select value, entity_id from CustomAttributeValue" " where entity_id in ({}) and configuration_id is \"{}\"" ) - settings_key = "clean_hierarchical_attr" + settings_key = "transfer_values_of_hierarchical_attributes" def discover(self, session, entities, event): """Show anywhere.""" @@ -45,7 +45,7 @@ class TranslateHierarchicalValues(ServerAction): return self.valid_roles(session, entities, event) def _selection_interface(self, session, event_values=None): - title = "Translate hierarchical values" + title = "Transfer hierarchical values" attr_confs = session.query( ( @@ -67,7 +67,7 @@ class TranslateHierarchicalValues(ServerAction): "type": "label", "value": ( "Didn't found custom attributes" - " that can be translated." + " that can be transfered." ) }] } @@ -328,4 +328,4 @@ class TranslateHierarchicalValues(ServerAction): def register(session): '''Register plugin. Called when used as an plugin.''' - TranslateHierarchicalValues(session).register() + TransferHierarchicalValues(session).register() From 3da8536f968c932811a7979680cc27980594c2fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:51:32 +0200 Subject: [PATCH 148/194] added settings for new action --- .../defaults/project_settings/ftrack.json | 7 +++++++ .../schema_project_ftrack.json | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index f9d16d6476..9d59deea3d 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -109,6 +109,13 @@ "Omitted" ], "name_sorting": false + }, + "transfer_values_of_hierarchical_attributes": { + "enabled": true, + "role_list": [ + "Administrator", + "Project manager" + ] } }, "user_handlers": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 7db490b114..16cab49d5d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -369,6 +369,25 @@ "key": "name_sorting" } ] + }, + { + "type": "dict", + "key": "transfer_values_of_hierarchical_attributes", + "label": "Action to transfer hierarchical attribute values", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "role_list", + "label": "Roles", + "object_type": "text" + } + ] } ] }, From d37497df10d6883f3b154f076ce91e7163c9d89b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:54:33 +0200 Subject: [PATCH 149/194] changed filename --- ...erarchical_values.py => action_tranfer_hierarchical_values.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/modules/ftrack/event_handlers_server/{action_translate_hierarchical_values.py => action_tranfer_hierarchical_values.py} (100%) diff --git a/openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py similarity index 100% rename from openpype/modules/ftrack/event_handlers_server/action_translate_hierarchical_values.py rename to openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py From 078a47e32f39b02c4c6c5db8860c2c7a6de886b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Jun 2022 14:58:59 +0200 Subject: [PATCH 150/194] added some logs --- .../action_tranfer_hierarchical_values.py | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py index 8cc6fa3a57..9df3b67969 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py @@ -153,12 +153,17 @@ class TransferHierarchicalValues(ServerAction): delete_dst_values = values["delete_dst_attr_first"] if not src_attr_id or not dst_attr_id: + self.log.info("Attributes were not filled. Nothing to do.") return { "success": True, "message": "Nothing to do" } if src_attr_id == dst_attr_id: + self.log.info(( + "Same attributes were selected {}, {}." + " Showing interface again." + ).format(src_attr_id, dst_attr_id)) return self._selection_interface(session, values) # Query custom attrbutes @@ -188,6 +193,10 @@ class TransferHierarchicalValues(ServerAction): valid = True if not valid: + self.log.info(( + "Don't know how to properly convert" + " custom attribute types {} > {}" + ).format(src_type_name, dst_type_name)) return { "message": ( "Don't know how to properly convert" @@ -205,20 +214,26 @@ class TransferHierarchicalValues(ServerAction): ).format(src_attr_id) ).all() - value_by_id = {} + self.log.debug("Queried source values.") failed_entity_ids = [] - for attr_value in src_attr_values: - entity_id = attr_value["entity_id"] - value = attr_value["value"] - if value is not None: - try: - if dst_type is not None: - value = dst_type(value) - value_by_id[entity_id] = value - except Exception: - failed_entity_ids.append(entity_id) + if dst_type is not None: + self.log.debug("Converting source values to desctination type") + value_by_id = {} + for attr_value in src_attr_values: + entity_id = attr_value["entity_id"] + value = attr_value["value"] + if value is not None: + try: + if dst_type is not None: + value = dst_type(value) + value_by_id[entity_id] = value + except Exception: + failed_entity_ids.append(entity_id) if failed_entity_ids: + self.log.info( + "Couldn't convert some values to destination attribute" + ) return { "success": False, "message": ( @@ -232,6 +247,7 @@ class TransferHierarchicalValues(ServerAction): self.log.info("Deleting destination custom attribute values first") self._delete_custom_attribute_values(session, dst_attr_id) + self.log.info("Applying source values on destination custom attribute") self._apply_values(session, value_by_id, dst_attr_id) return True From 7500097cc47b18f8d9422547d13500e89427dd0f Mon Sep 17 00:00:00 2001 From: murphy Date: Fri, 3 Jun 2022 14:54:53 +0200 Subject: [PATCH 151/194] added Royal Render and Multiverse updated Nuke Studio icon, Flame moved to integrations --- website/src/pages/index.js | 22 ++++++++++++++++------ website/static/img/app_flame.png | Bin 74845 -> 39096 bytes website/static/img/app_hiero.png | Bin 40175 -> 33079 bytes website/static/img/app_multiverse.png | Bin 0 -> 4814 bytes website/static/img/app_nuke.png | Bin 25887 -> 32869 bytes website/static/img/app_nukestudio.png | Bin 0 -> 37527 bytes website/static/img/app_royalrender.png | Bin 0 -> 11650 bytes 7 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 website/static/img/app_multiverse.png create mode 100644 website/static/img/app_nukestudio.png create mode 100644 website/static/img/app_royalrender.png diff --git a/website/src/pages/index.js b/website/src/pages/index.js index d9bbc3eaa0..f57fd1002a 100644 --- a/website/src/pages/index.js +++ b/website/src/pages/index.js @@ -299,6 +299,11 @@ function Home() { Maya
+ + + + Flame + @@ -306,7 +311,7 @@ function Home() { - + Nuke Studio @@ -374,12 +379,17 @@ function Home() { Deadline - + Muster + + + Royal Render + + Slack @@ -390,10 +400,10 @@ function Home() {

In development by us or OpenPype community.

- - - - Flame + + + + Multiverse diff --git a/website/static/img/app_flame.png b/website/static/img/app_flame.png index ba9b69e45fa73d3f9298e77ec4a167a38f7ecf7b..188153e573f64ae8f66d9a27092cb47b7049fc23 100644 GIT binary patch literal 39096 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 literal 40175 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 diff --git a/website/static/img/app_multiverse.png b/website/static/img/app_multiverse.png new file mode 100644 index 0000000000000000000000000000000000000000..c0d80e4f1bf6cec0d95451fe51172cd8e4a36333 GIT binary patch literal 4814 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 literal 0 HcmV?d00001 diff --git a/website/static/img/app_nuke.png b/website/static/img/app_nuke.png index 4b3797af7aea30fb37b43c7d94d91a249da013bf..1465da8ce823d72303087250d3fa74453c13aa83 100644 GIT binary patch literal 32869 zcmb??V|!&y*KKTcY;|ngws!2KW7~Ge>G+O~9e1aL9ox>1ZQJO)dCqS*AF9?>^{H0X zm~+$`bF3IO6+X2vm7FDF6fnB-8&I0^HY(Z(yS5*8|cWAS(g6GRhSEH9&Nc z({qP_K*suCgM`S=#fN|hL6Vme*YwFf&x6;+*1?M^8FD&j_9}~;^T#=Fv|q@=*I3ZR2-3e`(_gns# z_cq0Ul<$5?nIuQ^pGAuN|KHBDAwE7Hw@gpRfReTjIj3MTb5xPb@-C%j{0!R; z;M8aD;5r2FpguJ1p#G}TO#Z-SkW4&*OCwvz#vG+ZiCsS2085XtZy!GL$v=u)ekG

FeTI>>8KOkPC^ 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 literal 25887 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ⅆgv-9FWR6p~| zI}-GCLnu&us=;#7VPaX{Co4rE;#;7g#N(Wr?*6uw=#6M3JWG0*H~UeE2i_5dSK$_5 zj($e3fK7b`cD;}p^4y<*pKV?}z?R8x=R&$#qs4m?)>iAxg>YFy?(F~?qkGpK^dZif zgQ=tYc+2Q#!Nmu?Xwvb|iguQK1(ovXTN!Ptr`FRMc0&;p_?JtxSk=lfx{)d75vdhg zjxTW`Peal^noEEz8)jc<5vI=hyzQLJzzbi)oN2Opy5M${HRSKd=Il}u&o;7)GGq*^ zi!1in!DjApbO{p=58#`*Gi5_9D-LMcSK~}c34M( zBYb1JA`*GGN_)LB!RNJOR-~P2%U8_`M&9|m?7SJjFD}Yt^_6@#Tn6d6ksh?!faM4C zy2Q^O-b=A%vK3M+(80?0%lSMBo`Iep$B7% zB)`EGDex=>VK=-Ey0~#8J^~j=?1~7wtVO66yuXHWTe$~)$&E3{lZ^~A188Tt&n%+= z?Zo2#Ac2sclV|`rYsVciixR%Zygr)X<+0W_FcfWX_)~!YX>LgTCVwg^-@UduVZY7J>g9TV>Lnw+t^Tt@eOKy z*%Gk%GHH#sYcetkg$&7+ zDwKq7M%Z}mN@g?Y_2*av2FR>SKDL61FU<@T&8pwPHm{=xROdMA*~VX;{ehvLA~{fC9QIrdcLA$V1+ovRsQ#rH=z3W@QxABRF$(?AlfNL+*#W-)yr7QZw2iVO2CPk5En1ZSA+-r zi}uuq%gvoO8qF?1d~@*GcVQA+gI|rJm8*OYMlq*`a6J#rUl+aNDY<;fYVMM8gpdap z<1&#hdmLVYMQRX=%lPDPdxd4l^n(FjFKut7kC1hsQHfzK4`4m*MD}N|`kwGOhQPV@ z);paKRo@+iRq>2B-RD^E)rlD$yEy6l+}fpN?p?JPU$%<0HRXWY<2zhU;~;W=ULmTq zi7doj%pHEapY{s8975k|L&ZTEdhqrY%V{_PEtAep3|A@7_uEE_`Ab8>F0N*AYb33* z=OALC%HLVKz#isBc%FxC?SyY@01;=z#r)qBLc7IW>O$5vgu3nDa5})9t=!n`$q%#v z-dXwC1c#YD7`)j&QI!WIs-tQ(Qxy7`wB8;Wcwij?3JWmv*_}!<{93>yQpCwUgWQyR1^)`99ws+)Yu)78t^& zMtPrj35O_6tpVH;lve{OqxsZ!#Wv3ZB?ivoGBOiLvLDzJ1IlwS}@GUb z-dYV=s5G0ne=^by6=X_8iq1Qes%oP4?8r3rc^lqlg35b-EM-?CllAzxy;XG+}JNS{j3=cpr_5fnal48E`*JoYjba2 zWMdzSSoF=y@y?gnpPE&S@S_c~`@UKHki55Z#3GF9?W8u7*{<}eg-DM^ER!f6{r6Gm z@%rdLctU`z$#vo>d>mVg8ocfJ zQfg0u#E+8%i3T9fYQ7v}70+qYstETF$_kW+g${{bQ`s62Ls$FF`};DJ_~UE0@e-K} zplE~Y&Q3kFIn})sl$|KKZH^V8;V{1qgyr_D9cqHOCLXMWWY--!eAsczWW zUxDI|0^Dfx_7cv>8{LX#&7r1Bs`33Vh@pj=vDvc1M@RYBnlA&bYKVk0y;gc8-m6}Y z-eT7x&WZLT{pqn(LoU18RF!;p5HQ zl!kfVIW~F!=s^}f4c#sibZ0zhAWIe=IBHdP^g;)S+QNJ4oVc?Hg?ZtfwqH1qSFmAK zsqAx7BkT!omLl8>ml=8q{oJyx$-!nFkM3-3(3?G=SW`^bh)Bq~MBjx&S8^^g5lJ(T0~K4H#_5J)wXr+IqR-E& zRRPul#~0Tp*se8fVW6jlPWDAz0HP{UuV>EKYidM)|BEe5`Y)N=D}FSe=Y3v##UZpu zeuWxWt|)8BD6uT{S&8(`T%zWRC7D<&fU*0g=YAzC9h-4AWniqPs%EbDX)UMlY4ecw zG`SpQsdLb2!L4-hqWMhSH3pf%G~xCWD(e7E_YG?|rG1sB-U zsPjrv6Yci`YQ(UcwV@XpcyD=hm{6 zPl#f2IW8PAW_g0(B+WqWqrR(yj6H=P1?Nwyjqq-$z|Txg>?0wWQ1wI z(`T6`0?O5-`RiJ#ejRJXnhA%uM<;A6eZ=(JrF@lTua~)(GV8COoN{-~@j8CFv12PE zbShZuP^x(b728<1#xZr>U4!8MUKQnAUPoHhL}}B{_JVq})q}k88cPPRC(P<&dvXh; zS7L*>2)1P)iy+tLRwFaH6}Y=dALatm{mK%fY@ABU_sHBauY;n$l{X{niX1**Mn>8K z@XItp6fVDQeu5V3dFiL^d%fh{woKJWDlcLuhbI(M@L0LH>sQ%`Sj#mCSNEv81a$r! zx18L^o|%cH*vCe5hhYS*s=kz^=NyNP_?S2)ChlGLO&jnO?%or}B1B8b_y-c;!4jz7 zXMHU*cN~2aFZV3(0h_BNpj7`h`hwBs4;(7nwioJS%ZpfQhD z-hyVds+19_H!HU`Pi&w|4R4WtQVsw7ZeK;Wd{W^IJi@$(m;5xkohe&$Ov7QsG;;Hs7*5cOzri zlDi(GZMu6d->N9F?ab*YmQyz61o;&lT2lw`^x(ZbAsR{!n>P2ds2LPP_q;soR^f*& zGktfCDi>enX+*pfcvn0_2v!1A306FG35l*4^9wTMLAuEsf< zLz@45w$f;K7`@>?fl(&Z8Q=R$QeR9i*7GQd2*v+IE__}UQm$o>U0oJ9SP_iU*^Q0* z1zS+t9ldHBRWc+VTcUvO7xg4Ed#w$2%24l=@7Y#+szZ&T87CT%1McMAYeZrdmyg&R z0i(W7>gP7ilP?X20)nlV6EH$ux_X7x?C8%g6B*Xd8QYGPD@67U1X5Lr2b%XS@@u?& zUG1cCtSfCegxAfj)hwMBA5;@pv&w^44%sMOcBu~8{SZqv`O@yo^?tcGUs7WpxT=zd zhK}v^c~79OQDbCSuGa$(ELh21lUvq2R3?BFXcVbo$R?t{NFt!wZ@}OP-{wn-$?5IYFN81Qso2eLl)VqcgJCLdk`S8@5nhO-?C>hB}gcs)Pf!qVk=hNWJz58k`YuONe3`1y=|0~vd7 z*@#VpWzG7BUMMuMEDXsv=3gGPlb=F2xKp#CDK%H4)q9kgI^IY?=Q8$9a05#Y`H$!G z_X}2fzJ+=joDsZjty^CjI}MkMs??I6W>)OJN0g20Z#()OWD?3QVa!hCshOYgK@r_Y zIGHG6(kXu6QEKsy;@{~%v>XWzM_NYTU@>fb^FVVcGn#SQz}lR-mSf((PvgVlxFf!G z=2pVU_7gP~`S99sX4OdJe&1s8OZ9*c-bvaoP2;KLOKb4Dd&o(F^m&&=9^42akDe-R z`h9aP9e*Lk1Gi-zQ&=+nGPrUtKv#IIOZvPYJ>2zfSz(YHN#=Pvy-n<<;Lo}A3Z6i!$(%DDfQe25Q z|3PwSFuuk8?B|LT+YpKBcW@d!{Q(~OcR@b3{ zh289Xj&98NAwQw{j8g%9C4x`hWkM!cv3H)r)4>Vl2c|WMqZL|~$)HoNdX(-2EZ50- z42h3cz0ZU>x~k?Ls3f#Te+q@47J2i7!fEEq#oO>M7Kw$3%EDaLM=6`%SFId_S#!}g zp5lD=rhg(9yur>>8*V^n8ww;} zUF3|*jSW^uVe2;Y@Db{@2cQ9%^>-Y9PBUzEdl6v+@T9IXj1QiK?hg%)&Ogiu7+pLcdFB=@1A zq#7eaLaao#@Q6LW!LOV1%#<8JlUN@J$tWM}?>PfyjM%^j&5bKp?4Q@YsJvd4B7+g3 zPD;V&@>xzDn^?g*la)p6QY!cimTdqof>xwir$8- zmgdL0MKh3r7+I4q@klEep!*w4q&ktlT)Wk-(v5FH;b^OwE58(BHeN{DwaRY1<&5i# z2&=ca2Q8II93QJFYF_sW{+fm}WvV#t`|#uCQW-aT!KzCBW$?KSN8*;V&v*G9&S`!F z8h@EqI>-LS#i}C2w@2oER(vVJ3A69vF4(GwiuYbTqgqzaS0+}ekn)g;zt7|G zb>5$fX>3ZxoAjtMHwZj%nSWMb*??czHAkey8vrXBOJ-%+fSJ+Hd^!-&OJjW2Q7mMC znwEfmwQ+^dvT=;0G0Z-smFBGXyiwn`jmT4WzoaRM6oH_)gf(O3$MDs{$y6BX$_GCAbhX|eFnaYS(b)7$ z2FlEf!E3%0`UuX+28(+GLSC(&6TlNM$Q6AYW_Dr?q87zS=jcc^>(jgv5x#vb{e=E+ zJ*bM_T5DBCMhew1P{+Y@nqAn`%#Rv4&=X?PDsYfZF{ zOn-1#s5D^XbH*s4Mmcfx;%O5(w}X$|7t}w|$8Lr*Z3C{@RfO3OAbfgJI9#vkt*=}m zkiJ2|K6bFc<+Z2mYNY?N;b<>*Ek@wWAXD3&WHPB82|!g{U;w9os1){^_t(KW8tPST zavJHvU!nki6FKn7)xDja0+=D8Faz)zz{c|N5?nsMa5>e(jf zI@@sQv>O~~EyjjEDAfHT=vOZqF-4R)j%QvTE)&ev;x>|lQlrXLu1Pt!%kjLEx(V5A zz%&Dnj7J!>}-^!Z~PCnlY zF)yH9H2e8b%InK2EaJ67b7T!URE5&gX}<&O1LGs1;r5@UMt)DBmcHohVq-A40p=w? zg(@lBJ-*TbB$^VPprq@@I!)w?;9Vqq*kbac@Ktd-qf%i!4e0w80CGQOmI|9?>d#|e z>a{r$HlHlMN|?`W7O-VAXtD!ND*w_SUh?_O^0w|+=Be_v>F-r_DR9IVM8 z&;`@~bs&#ArYmLn$6Mm;)#u;JulHzfKPG*lG{xwt|EMEq8#en6!aT?6b@OeS zW&0$ZZiOgErQ9+V6N(X>Ri{@!8&t8Csdp%}sDBU%Z53R?4f?n{R$XnL#_=Hu6^3RI zW8;dF{f4KE?)L#6dYWsjdfCiIf1Pp?XOhmk7&umGmko!uJ5e7u`yXcFWh1*S5BB6E zJ`jr(<-a=@#Q3Ic?v-LMHYqL zBs=6Yv($V_Qv1*z2b`$$?GK#7^GMGmo`ZI=xg$8gCx0Wd)AU_6$K)yHrN-+~VO$ezyVQ~{Kz56wS%eaWN zyS1Nd&T1YDpWV;{t+Tx%x(mc}?&rfn#m&QpY1X+B3=|Dw=dE!M6kjC0bDkaGd?Y5T z&UeUb8fM@0`0?BwquiA$zI>hRMOcYY_{v)k2wvj9Mp&uLdH$vZhr)Xw(a@ZAv%r#g zA=3p@a%R`WPtTylI9s!pXnNz|l3yC?o2=^XsB-$H@~mRd<=VYmm$CN#9$*@7=g=?5 z<2~?xogKo=G8T~(!+MEnu^>Ce9sJ$?k)gTaF^^EC6tl(20Jo=8CF*G@I8ryqMDGg{ zp*+=|BNTIvic%Y>m8qCFtM4V{j15_RWT=@4@3HL_K<|7i#q7>!f+)661b!eF z?Ese#sMtSa)>^{M<@~L0fd&GMEh`Uwwl$&Yb;L>viVy#dco&(0EE7F6D8Ai|K6-8GJzKgx)PSe{U z{cv$>8G3|O22vDdE*$Wpk1hoNArEtF;FE&O{E^)`iPkR{*wNle_NBbIv%azb?T=j~ zia+()OZ8=X3NzIvLQLtUPD;&OrF^piIIc>~^wau<;~|9Pm$b#8rY>U$+`3-AHY~m? zPcO38q|m8*V~CK2Wk%y2Q!;WdI-r~!+s|7K^EoSZ`-t|Cb7m$V>4N)pudzx3~xj}DQ+G>4}&Za zdF@b?)xRVSjFe7sBL*bQBh3#_5jwkjoe`KN+#!?n3cnAtl}Cc9lA*;-me$Vl>e!lf z_n2XBm}`+c*O+q^cBxIkV!s=Iy{jIYMI2IIAcAW=v#PG?bx-mp%a+d2@By3stmXFc zt31AE$rdoQq#nH(@#ZV9>>OoRGh^Uf|4g1E*zrT%>PSm(+n3YLa;`pTEbc2Jeg=P( zuz$aQu;ko9cCqLsjPZrg!|%Guj%BOoR1e`$>iTf1c)l5g3*@}!?bpe)GP@DBYl7>>4aXbsro0QLqwN_{TTq42V zn8-3D4me^nO=q7$yoDn&PO2WPjOh{!jCv+7(>rQYPO)5P~k4sw2gk<3yVCd}PS zhk;Y-Wxy_F#`i1QrJ0;7w_xy0XJ|9!TPZ^=wsFZDmYKA#niU5n!<5c=WNo(V9nsik zngd8J-|p2LE$UEONmZ*x?gPcPGYw7Vqkk|2llfN~M>23(jOZy5Xms_7#wWc;%I&!t zWyO`nVBIp8_sn=zT;{EU37LY$x&EOL1pqg$=S|%rv$qr(l@0fy$;EV=hLpZOKED+u z)NX)ubn9~WC2uZz3-@1#A7WRh_)3U9Pfp(y;0#?HHR*Eb?0GU;hYd}L28Fk4C>`_I z(Q#_!Bi-6>(KsVc>z(h{zUUq*I-4)jf@N>=XF4?@a~-99tFK;J^<(#u1Cgk{A;PzVPk7@num|D zY8n5O5BK2x=KF{yaoZFhm*Sl3*P;wjt#Or@nJgP?7_k;n!8(!<8sI!wVv@j5Xc#_w zu>uW2@u|Q@x8)OkWqypIW3wpxQx3OF6=?CG@=W?D7=rfzgWsZC+xS_;kE0Qg*d9EmdOC>mVeHYEo>_{P8Px;CLDZZJ_=W{6I{ z5%?Vr-l;OdUJ!_QF{>Otpkajw2=)t=tDsm?d+NMz4>}ypy3S&+G#y00c+Uw%%1Tk| zS2oKDKU@yNC~~g|ho_Q~iHT|!v9!?D;1Z_fI1UWQU(2?Of%jEZ`90<@M@%(E9{7$P z2XSuv?h^J%UvtJbhBeB6;;LRS$pN`Tc!jTm`ZOFoM^hLV>PGu3fQzT@@+Njw>8`)1 z7f%IoLIt0Lkj_6qqd90_Ze>CCxru9UZKnZ?!{GX32KoKhdyZQ8xnwrnU&NWDFR_}BNX{Xm)i_=TzzL!Tgcxspoa@`=MFf~e90gV zcoy?-+F+}X57pz_0tP?Ct$(Jt{0gnnwg81wh}BrR5g>RaZ`ja{CTU>-w9T8#F9IvE zMAPPWwxj-7`kZoManLhKk-_H%e7WC>CyFLXLX~prCRi3Gb4c?}LX-Ij(~T=96B#?F?$vX-41gNawXH^K!ACIVyEQ53bL?nXQcj=pH*y42FZ> z1Y)whEy|S+1BhOVr6zegjv|%meVtfIA{8)AC`eK zo0R78;6y1?Z$5P!Vp(I8x};Y)64o&OxeCZ{9xOeHlsgR?1^y3z!=M zLXWtu%w;#R7+5knVr5xOl-iT1nOQ}4ia-y)vDW6}r~pDPG-)8EcCa3wBv^!UoqDzA z-t!avIj6j(SRSGzgYeDF)^cL#BP8%+1k#$!f@{nPcIUJKdYv_AefK1}mv|zSS{;)@ zWbX5d)d{8Hf@g@ObN*yOml(2Y>nZQ=i*E@ABwl)=LenJe{)X^`u-s=`6iY@=89O&I zYHdaWW_kK1I=QP+50=hiouaRe)KFL7o_(F{@Y`Y}bOam2kXzLlDzm_c$sohj$NV z-~2K(XtudZAifWeV)f&HGLg)>_`HT|HhzI?W=v`ytB&HT3?OQT8$CRxn9BXGUWMVf zC!~+xdjbq$izM(oaepPAkicD%$&vJni~hOTs27Vow(4I`v6sJaBBu zHp1+o(=8bKkiOEuHISG`e#KG=S~iLXklyjG*hD{ythv8GuEQQGXG=}IS2VJl#FsLh zcyb_L@qgp(!*{VHhB68;nH8+4nx7b|71^9*!Mn~KSZ|W@QziO_wk(bK6+I4rs;@)9qL`H<-Q~H zG3@9Z*~!1Rmf~H6%N2(wuZHZ&Bg-f50h`_nE{ohm0%x3NriYQ&lpl}q>Lv;L9nv=+ z0&surL}<5tDgE3iau77qw`E|z`Z!)z5ELqC6!{cwi`@Lp)RcUDXCM1j76&za7+rql z5xGt>;g-DE=R!x)Qr+QmyVRKFo#wGfTb1Vv*q4u{VXHAVAvK#jc@8&fAwCHSeWr_r zzaVJ*i%Af1Kkt#nv+HYE!i~dF_HAssdA<(ePOpeK0|SS4+deHN^p`Qv^lANc*vJiq zA{y!)ZkBoE;oeS2=Q+itjRu_ey2>Gg(;59_re9KF{H+=I;t-L zQdya(FH6HdM42U=hMwMHor1k%hK7SdU5B0WbwB;x=2M_mM3o_i-!A)2(mjbU-MG*G z(wCw5pKgQvs5`2W#D>t8}YkC}Uyi`Y0-#C9+hu8W!O{pJFp9j0X z;Mfj#j#B+e*rB98OgzPSc`oCn!u%|A%<5N&5-VuJ&=8q?+sV9&?K?-pmsfMeyj6}z zP1xZ&uEePPPzrRv(-Bswj})kyJF#;O zSPGQe=JqL^!@e6A2{teKD}@yxU(6uYI#)Ha zV(*E9W>sssK;5r_nTD^bKStdBX0b*BPl2AbZySI6#&AI1&#*Y+7X_=ERaIE7$6+1rbp zNqWW+H8VS%jQ!ryeP`A{GreuGq7~wh&kHcO))E-#hKaN@=8LA6vFitDowq_8l~oRR z{oe`F#{^52L!Iaf?(Ke0Oc@HnP;{3@8*m-^OX?$t^Tf4VMSa#ILvA`j_@vJbqd&i! z`kjW37q;gybFD#v96N8Zfj5+@?MoT-87(4MD>}t&tTiY4xLC%BwSP;fb-VN02{+_K ztPJPKLt>$hrN3a$JTZEV9M$VI8y3+BpzdypCOk z6K=JBC5{#r`*LI5y7=Y1>ltr~vr ze!qAbc0JIZ+S|ToppJgk&Jy)QJKUM6JE+1y2=3DiZuZs;lr4uwM~5%?K}{cSR#*Xp zhC7jtYP&TytzRY)J!ug$X`0~AmgRr`CJ}#Lj_s~{t|vy{P$B6#%JkO3LwLN3>5b#X zTgg$|D6D|WyDrl)pe7$dcqE32%|C-JohY35;mnYcZRfYXvC$V8)7G(y7X+66t7bdJ zUcyA`lhwh~er)JDblv;yShKbP!PLbh>g+Oc${Xf8hlTtZ{EwhctLMdesL-9D|n(xqq0vwhS>!R3JW7XDY#b@xCSx<)9QT(>_ zi^bR2?RqtO)=3TOm$B2=FbN4}hOH199#2 zZ|$E{(`nSgIEpp{1poX3!=Wl|9o)<)j!kmTcYMwrty{Z9ZwwQfjZ$5^s+}|8?uFmR zVuw71?41W`%@c9wrBv<(=QO5zryF}<=pP#aKLiRKj~P}}%Cj}w3Fp-9#a82nkKN6- zZ0fob-hIr4l|f+tt3o#qfIMiOy3e$M zV&0nOkP9ex4PX$I{ZM*oJ81QY9r3LJcMdHz!=FjlO9?kF!K3zP{so=V8Or^(V`*v$nH;$(a30X+r1 z&9qwltgPcxAqdSC^v?=VEeI>|&pKC>J6!4XY#Xe`dDhMJ{+!R5`a(ma_hKh!`#OkR zj$yqCFOBphbmpNDV);}Sd6Q86CR6Vp}T1D<)w*wP!I zZ=>*vh5_O#2%(+t1JuF$zp31y9KEKA8}X8lS9_EwT4a&>KU(|FZ@9iUUY&_PTB1e| zVwHOv${8Cik*~l{0wLR(2KXNpaRscjN)9Bxc^9%FZmg zh9}*Ikt|9SJ{!w#z=MwHrAFDu$wpbPsuqXAH#89bu!ML$5VuZka7*qoGIzQ6UCQFC94@J)pL|mNt-p%*kKN`;ka@A++isVT zvCHeLN~JBuqqSR5<*IYpKz?=SDh2w3?_8dsoo|)*F8hL+UOasmAG-oH?3Uu3dST!8 znB43-yTR~>4<^*gn}C#@IeV}bXnnS5yqN!P{4Kezv#_ckJ2{J0z_ZWQ1*{I64DV~H z@t|2T&0=%$o))C`HH;~_@d;Wz{@X`$*>RfsbopNH%oCLTcif8T>`y^TOcSXgEWbc+ z@u9~L`gOX4_}@#A6AHAl7dYL)_^a;#v})kfSM1Y%J)?6PWB!7ub5dl?S#R;2+N-?a85ffBCAG~5?fm;l$~ zw}rE(jpCKB?@c~8AsgoB6BC+;y^CqbP6obw9Gk(rbP#xW*lH=rV*2QANL>{D~+(?5*L zGab7hNcqym6y;0o&v#c|0KYf-U-vb5DE;tygKER+S-x|Q$_nF>qt*&b6Q2M)Y0%*r zZN2&|@WQ@&*pdYLU{3MkD&geiOrgOS0vO}VuF9W` z9+c`!%(0^Qh_G4|^lfXpA1AU>SPyHZobEVsN2!bu}7Q!%knB96j^v?x(->X>1# zuHE|+?y-ty?%|bmirL_vAPf-FYYHZTAPZ!FA44=Otz}(h&fl0OGkaB zXnEj<^`Oe7Cc$*2EJ1KRH`&v*!1^Sb1S3ISfWXBZw9kaziTv}?edAehU5F?iuc6Br z9jGQ`bQ@u^JW71^Mtah(EQ>ssT%;mE+0vvYWMxk);Ryv87^jO5_&Bu=0Xkr4(|eMv z-pvy$qZfSd88YVU2TVZ zkY|=CP>`K;;C;mf#cZFRgCeXZ!tK}#-MlYrGYA&AH*=>DEUIQTZ#iTzvD##SZ~XDn zLSIK0Ez}6$-lZXc<8TO^hU^m~&_~m*cv7=SKU?-^`@QkDSJtEBF@^UU0n&2r)0BPM z-V$3gDQ>jk9L2xVWKcF5YKCmjK&ADPw{9A?k%s~9Zno*Y8%_+OIM?=5Wf>Wu$ zzC`;x5#LDOT@}gjiM45HIfcG5KJ5SK(enBujW-AG-AC7{0L}E;43zmV&JX(D8h6iR zSF`%e-{+CvUyQC=D)(TCi7{=sl*dW~{C9w6bHoXk!)oc8ksPOm*vM=8Ohl|y&~}0( z5{)(KcAaFx(M{ImxRNqc)=UPM4<#DW^%=!cCO=emN@0M6%CsxDXnLw|yi4!&2AUbH zTjRUuz&yUHvm>n@6XQ>fuRNTbds!+hx1t@dM@dNkUT}{Jyiu%7EvmNTM;54@&iYN= zK%>%ooT~9lYz;Fbe4J(vmQq3i^>Vb~k|4s46lGl?e#|U2Ali-*s5s%u7LNLNFPDw$ z$i?C0Ro_v&`Rr$l(J!{0P7-XZP|BPK*UoIQ9#ruB@ZAFSWy;+xiYkBExX1~opu6I2 z8OjPoy0||=k?|4LQvPLpIKX0o_LKGuNkp^ ze{Wa<0U+CW+w1>rY4M^EKA)OD<7~~qM~LM+65paHZ;OjoE}yie3KuyVrQqq7eQk&l zzngA1pteI~e6=ptFZ5Jz*!)*T{pA&~vf$D;_V=f)itZuqSUTGCRS}A_0Yv2Fh#=}x zoy09J=|Cp~kFw5i4lqGL6Jkz5z4q6Ss@CK(B{}!-YEz>pC@!2Hlym15LVuWb;58v5 z6;#0hHqO&YAJmhHZ9VhlM|Od0wq+vx&!g&Z8KUe|6{Dsx+Rz__i0xdG4QfG^jz7V@ zV6xRFZtlF{`PlK2Lw~41`GY)Eodu%)c7`0~ymIvPS3LE(BPlu zQ8UA0gb8Oh=wa%qU-8MrB1q7B=#&@fua>`){=IKKPcsNm`MuX;u(P#XwK)Ft{>yfB z&QIbj8kZS?57$Z1@eUf%gxZD>&PNL-Lk3I?{W$BQVIXbHnQ-d6q&k@*Ib?2`PDB|% z9;yPl|70-UIuLZ=4=tful%jNBGU#O|xKlbRvc}+ ze!s@2P2h-TLJ5=(qg7vYFoTVxCs~+}Knbdv+@wBgJ_*)0ffLF^97()bvP({>I``^U zsacvE5oZL|-ex;i8%B&RVo?xUkxF-6vSL>;NYRxh+Tev(R(!Y3EB^!5Q9R zy<-x`=Hu8lo_Blc0ER^z zK8CLD&{b&SYk^pZ@{25$AOt7ihX;E^)$&&lv)=Q1$Yh@eYyN{|Vs1=*R3U|89zXK5 zf15_ch457{YsFmdgKj#pD{MEXYC?S{TN@xr@=t<(N^$4<13BwIP}Xzhn~jL}jt}`x z6@Di{Q}FHQeO0GdE3cq$*PUdR^M|DCCOYzh#DY~T6HkxHpm*m+<7wnWmjAIn5Hzx{ z-d%$Dzs&l$p36@H`r0b;Xk7aC_d9^Z?pKOQ$8T<$Q0<))&10VzgmJ;}oRS}mxrtH~ zTrl!Vb(+Sr6_*7J_nUgWFcmi*g>um~w5jg+qAdro#DKd;98DqG&uodMGW3~X4q=c0 zN2~2~Bi)luEWy=Sw$`G*${mJo9dn{N=3_7I&@%l_?A}bVC08QUXF{eE-WFwLOi|TQp9E-fPuS0A_lBC&Tj^;jD$TGlhJ^F1| zCy6Z@?TI)M1Q$l&Y=MU}+B#{C_voF*iygfqJ|e19du$BJU{;W{+6v#)$o^E5=;yB`c)9u3)2P|b7naKW;b7Mi zWhGbvUkYy-la*oxT0FcF&5k-Iz$L?oN+O1Z5!FhZe=AEHNAm^}g-5nW6BxMF_8DoY z{nvgom`7LW@AHuBb&TW~q27U-WUu=@CBq|g+fSsBbtcLqq?VEf7wQG{bPf&!z|2h`*6L` z)z&JV_T*f^NdQ!a%F|6+B|2%&4^S-`r$I_*CI$N`ZI|TbT95pGW4`!G@4xMl3meOj zY54e*w3CK>=)Uf4sA9F$yol_x;tzPWFcFedO(FVdfN$%Ld(#R%F@TnAD`okY2K|-K zgFInOUh{f|aHT>`1DE(r8+`c!9zAG+o(gw0nBS2!-RQ*ax)218*X#YT`PqxOn0SlS zXI* zrAj;EY+cz!w5|N05b26Y_d$7na^>4%sQRAWCa~NK31at=QU16a-B%?ZInw{M=SloG z0++_2is|X5wA*@TYEL_?$S-!%ZFnH|iyWy48LsvqAv`<&xwAOH2Y~Va+-6}(hy4{( zc?!Vmsx2bc8`QwyO?8T#^nv#?Z1FR9MT5WJ3ga^FT2AFL_H#M9ZL)&$d^uG_EXDrs zPpE6EO_z^=wA1EVWc=wTE-zoE3kbNuszh>IPzcF1$1B=opbqn}v6n+$_~5FQBfZKE zP3DgbwP%aWf7#FH|Ke+0Zxt?G;a3nE{|QQjiy`F{wZ81mE7ld4A~d45lb;$>2hr+= zig@Pj$LF7TfU@K6ID7Fq4<^Sl=NEFXWV$Y8#NyY47v)l)f|Vjw^N>q=Jik(+y&rMx z!r?7%AJy@Du(EQ^v6=1XcRB>Cvdu?{^*3-6iWUhD@nJT{*TyAJ@EY#FQCf-X0q)%& z(&3<0bpwjPR9NYLtpdvxtTg;v1c@aNIn;n~cL-9@%ueE0slfELkn2wv{wlcukuZIL2hW27cHLPae+-46UP2!jv%TF@4$Y@#s0= z$6H(wA$z{~sJ}3n$qz3rbvyK(cDx@K9CO!Y9W`ifL)r>#TP8Nm+%kaP&3xl!JCng+ zm2DFf#`WZTntmE%A8U{)H3~s239urUJeV{j-a6)UYbS13ahF(de4UUih4S^>`QAKj z{%1KQMFUqK#9J;L5M>oc9r#)INki0Kq}G}RB{{T;SoaXFy@rW*>E)Ntn|kf$R>^F< z?}PfBjj+j7Y2_4KHn#pw2{59YuW3Kj&eR2t2 z?+tJt_gKds=*gibWF-R=%uTg*Tj-Q%n|q$~jx=gu#BZ!n7-5PeGlJ{5mq8IbDHOkik>!W`Uk&2>%PR~*vT**z@%F?rp z^bP3ge2y?CVl1oe!aJ-KUKEo0-msLjI<9sH4;MxA!!K7t|=`rH35@lX}=d30anZ{3(cg*aw znb%lM)_#TvnQ%f++C|+ud03Tftn&5A?PIFDH=~FVl>;N}1-bKTAD>X1f!sWNYD&tH zIA}6)R~yY%z{D*_b|}sC`h}Vp=8>&3w|wHkhLqb#K|z3JJcy}2v5SqOOf^dM!%Q3S z#u-LB+*I>PgYr9vzjo8SZF#AZEys3)V{^l8vy5>6ao1A$h_3EiaejUJ1*ZN(r0&q2 zg1(iP?XEFwKeY61dC?iB(44-=Z4qyG+guC(MV& zr%1RTTh;YM_cb5KLfKpenYz^b*gh>QKD8-`iX8N*Z}BM zURxg8r{9Qc-%3yc?C#9)jB~El>7uflX#D-+IV$7ejYpqv{e0;!1}pnxE~7p3z3>)* zs)nTtaGueH*)&tZ`(F%Y$BS5!3yrK(ewLK1_fS@-XzrDq7UaNNBlgq!k2t-NyK#S- zAcs*17aUXZE}s%M-hCnW#p+O~m;Y*xg?AdQ5c_DMcZZoCzV{i?&JcQ}iyf?$ysgws zR;3)yAznbj=cV#JSN1U$nz`BO?nr;JixO|#&k=TNEN51HFVK}`P*-F;t#Si23qix!C4LR*>)XUP#y?cdw_v)9kr&4iCIHB^ zr71xRt*C}sL!tyGXdBKu9MnZ*J)Q8$*J*ZBPl3k4*$tTHc{VPA7N1sv&h1oD8a$ds zOD?mzF#q?aHwY3W$uIh~A}K+XrkVRNOLtChrpOQl5^qeDE|=X7b9Jb3oqL1SEphKm zXaGOXJBx!^-Yp`~o1d%wGpnf507~S&s;|&9A8!K$clMB(<@>&WkNI8r$G_b#>J1ov z$|20urTB}bTP!F8H5-%Jt?&4x3ui1b=Gj4t$5EqxZ{E$BD|lfC)4jt8H>uMT*uofD zE;NMCjS-Q7f5}B!1t>c$C-EIasLg-b{O5V6Y!hOn8kwM@U21+ezqr$0C3BU}vh?bT z0G)T9{W2W(h)koBDeG*SffT|52Z4bXNi&1!hwA>0kpz^vb&=Orl*6nhlni6N!M302 z589Bd-*Ze!3k}Po@rP-E%v=~@X71;0PsaNADy24{n^MCqaI=g5=??3A&o)5TLFuci z7%-9%Gi@k9iB%$u ztKUKsLisvv%>O3GCXzwT8DXl}UGnAK@S0E7@^@HTJOZV75+7q^01YAb#LB@ZeF8aY&iJCVO&ldM!p|TckI0X22DKhQ3I_38v6I- zgr`*gl;hrQ2(>py|4o{hy;TvxHx-dk`^=_#r(6zx7;nP(^|*FLU&;N&58?BNKkr#a zvcEQDmaVmFNpMZ{F!^J%L+W5^oca`yzz2dr$9n#$y04clW>0! zf2IX~S-I3=@4}yQ6`w?9djZYLMpDh3>3$enxD0bI5=6g?il%43nL`tK#ldo;G$K|u zKVD71TpL+bMZ>bpU#9EYi@ETlNeoRjyiz`t$)yN<5u?V8eiy@4&gX(4wV=KEu?&sK zWT4Hf3*2=j?(iaO&?W|V7Ooro#6$b{k}Zwb+s`o3xqXx4a?$w*#WPgUpcwA=Bt%@9 zDcI+x6csu*d@q`c4uT;w^`%QsAUs+r%z+6oDlybav$<;S?e}|gXAabJwj550)&M>R9va3dooYvA?jtYtX1+(7WFq)eXD!KSL zC*o5kdPp-3=HNWa8qRkJYy;6NLJOv{wbpnUB8qNSiybyVIP*|qH=;<=jG?se?9h@h zPNae-f5FArIU@;0Do;kZp%EUEL@EDv=Lh{ZK=3p-%(QhSn<6%oG1FVS6jK*uSKV|g zSCvjrEG?Gnt9csfS3f3vNx&C{oe2A>@Dn}42q`L(g~7hjWz|2;j!F^ha!r2Q&9C!l#D_O5M%2tqwx9rETD^AxM z>qyb@LY>VjEiz-%8TGFWC@h{9MP5{tvRxFp)9|~tsvKO%J7>1R9~{~p7W0qY zfYSrKqZz$*q+EMkwv*ye23}EcX*hXYG#%>N7=ra(E)h=L0lmJstB8BRw1}4Tr=F|i zyKrqMt8m6)M{n8(Jik-KrtQt*V#M`g8x0I+)c%qes}L$LmP~Res}>nRB5((*AU(jE z_k`VhdMdhYqci_o^Q9{1p*2EbqBmZ%k%{YDAJf+d^y`pDpIUE%=S<_-8bK`i$UeajRbOaPO^!yzE;2Z=l;Fia|)4 ziZf>ptuCo=Sk%j99~Ulzr2`%y92!n5(Vppyvs$Pc!y6bA-oy#s2iz<$)|y0ARFp;p nfBE0_|GEAD{Gcjy?3&ol10i7e3)*z^7be>325MCho5=qIvO9EJ diff --git a/website/static/img/app_nukestudio.png b/website/static/img/app_nukestudio.png new file mode 100644 index 0000000000000000000000000000000000000000..dfc2ad5a9745a5010589f7292a8c16f1097294f6 GIT binary patch literal 37527 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 literal 0 HcmV?d00001 diff --git a/website/static/img/app_royalrender.png b/website/static/img/app_royalrender.png new file mode 100644 index 0000000000000000000000000000000000000000..0e49519227003bcc031b8267315266c7da9e3f9d GIT binary patch literal 11650 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 Date: Fri, 3 Jun 2022 15:04:20 +0200 Subject: [PATCH 152/194] hound fix --- .../event_handlers_server/action_tranfer_hierarchical_values.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py index 9df3b67969..d160b7200d 100644 --- a/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py +++ b/openpype/modules/ftrack/event_handlers_server/action_tranfer_hierarchical_values.py @@ -241,7 +241,6 @@ class TransferHierarchicalValues(ServerAction): ) } - # Delete destination custom attributes first if delete_dst_values: self.log.info("Deleting destination custom attribute values first") From 12579501c16120fcea663e97db93f5dfb8dd012c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Jun 2022 17:37:45 +0200 Subject: [PATCH 153/194] global: fixing color metrix scale argument --- openpype/plugins/publish/extract_review_slate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index a2cbc1b704..01a7b0f592 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -189,7 +189,6 @@ class ExtractReviewSlate(openpype.api.Extractor): # make sure colors are correct output_args.extend([ - "-vf", "scale=out_color_matrix=bt709", "-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709", @@ -230,6 +229,7 @@ class ExtractReviewSlate(openpype.api.Extractor): scaling_arg = ( "scale={0}x{1}:flags=lanczos" + ":out_color_matrix=bt709" ",pad={2}:{3}:{4}:{5}:black" ",setsar=1" ",fps={6}" From 248d3bd1a36630adc42d9ca090f73c6557498d1b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Jun 2022 17:38:29 +0200 Subject: [PATCH 154/194] Global: removing duplicate timecode argument - this will be applied at the end in concating stage --- openpype/plugins/publish/extract_review_slate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review_slate.py b/openpype/plugins/publish/extract_review_slate.py index 01a7b0f592..cff71f67ac 100644 --- a/openpype/plugins/publish/extract_review_slate.py +++ b/openpype/plugins/publish/extract_review_slate.py @@ -173,7 +173,6 @@ class ExtractReviewSlate(openpype.api.Extractor): self.log.debug("Slate Timecode: `{}`".format( offset_timecode )) - input_args.extend(["-timecode", str(offset_timecode)]) if use_legacy_code: format_args = [] From d1ff95129f447b52c46aa158c24b2c5d9ecac325 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Sat, 4 Jun 2022 03:43:28 +0000 Subject: [PATCH 155/194] [Automated] Bump version --- CHANGELOG.md | 18 ++++++++---------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6613985ccf..50cb1d423e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,19 @@ # Changelog -## [3.10.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.10.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.10.0...HEAD) **🚀 Enhancements** - General: Updated windows oiio tool [\#3268](https://github.com/pypeclub/OpenPype/pull/3268) +- Maya: reference loaders could store placeholder in referenced url [\#3264](https://github.com/pypeclub/OpenPype/pull/3264) - TVPaint: Init file for TVPaint worker also handle guideline images [\#3250](https://github.com/pypeclub/OpenPype/pull/3250) - Nuke: Change default icon path in settings [\#3247](https://github.com/pypeclub/OpenPype/pull/3247) **🐛 Bug fixes** +- Webpublisher: return only active projects in ProjectsEndpoint [\#3281](https://github.com/pypeclub/OpenPype/pull/3281) - Nuke: bake reformat was failing on string type [\#3261](https://github.com/pypeclub/OpenPype/pull/3261) - Maya: hotfix Pxr multitexture in looks [\#3260](https://github.com/pypeclub/OpenPype/pull/3260) - Unreal: Fix Camera Loading if Layout is missing [\#3255](https://github.com/pypeclub/OpenPype/pull/3255) @@ -19,9 +21,12 @@ - Unreal: Fixed Render creation in UE5 [\#3239](https://github.com/pypeclub/OpenPype/pull/3239) - Unreal: Fixed Camera loading in UE5 [\#3238](https://github.com/pypeclub/OpenPype/pull/3238) - Flame: debugging [\#3224](https://github.com/pypeclub/OpenPype/pull/3224) +- Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) +- add silent audio to slate [\#3162](https://github.com/pypeclub/OpenPype/pull/3162) **Merged pull requests:** +- Maya: better handling of legacy review subsets names [\#3269](https://github.com/pypeclub/OpenPype/pull/3269) - Nuke: add pointcache and animation to loader [\#3186](https://github.com/pypeclub/OpenPype/pull/3186) ## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26) @@ -49,9 +54,6 @@ - General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149) - Hooks: Tweak logging grammar [\#3147](https://github.com/pypeclub/OpenPype/pull/3147) - Nuke: settings for reformat node in CreateWriteRender node [\#3143](https://github.com/pypeclub/OpenPype/pull/3143) -- Houdini: Add loader for alembic through Alembic Archive node [\#3140](https://github.com/pypeclub/OpenPype/pull/3140) -- Publisher: UI Modifications and fixes [\#3139](https://github.com/pypeclub/OpenPype/pull/3139) -- General: Simplified OP modules/addons import [\#3137](https://github.com/pypeclub/OpenPype/pull/3137) **🐛 Bug fixes** @@ -64,7 +66,6 @@ - Hiero: debugging frame range and other 3.10 [\#3222](https://github.com/pypeclub/OpenPype/pull/3222) - Project Manager: Fix persistent editors on project change [\#3218](https://github.com/pypeclub/OpenPype/pull/3218) - Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214) -- Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) - Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202) - Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185) @@ -75,14 +76,9 @@ - General: Oiio conversion for ffmpeg checks for invalid characters [\#3166](https://github.com/pypeclub/OpenPype/pull/3166) - Fix for attaching render to subset [\#3164](https://github.com/pypeclub/OpenPype/pull/3164) - Harmony: fixed missing task name in render instance [\#3163](https://github.com/pypeclub/OpenPype/pull/3163) -- add silent audio to slate [\#3162](https://github.com/pypeclub/OpenPype/pull/3162) - Ftrack: Action delete old versions formatting works [\#3152](https://github.com/pypeclub/OpenPype/pull/3152) -- nuke: adding extract thumbnail settings [\#3148](https://github.com/pypeclub/OpenPype/pull/3148) - Deadline: fix the output directory [\#3144](https://github.com/pypeclub/OpenPype/pull/3144) - General: New Session schema [\#3141](https://github.com/pypeclub/OpenPype/pull/3141) -- General: Missing version on headless mode crash properly [\#3136](https://github.com/pypeclub/OpenPype/pull/3136) -- TVPaint: Composite layers in reversed order [\#3135](https://github.com/pypeclub/OpenPype/pull/3135) -- TVPaint: Composite layers in reversed order [\#3134](https://github.com/pypeclub/OpenPype/pull/3134) **🔀 Refactored code** @@ -93,6 +89,7 @@ - Harmony: message length in 21.1 [\#3257](https://github.com/pypeclub/OpenPype/pull/3257) - Harmony: 21.1 fix [\#3249](https://github.com/pypeclub/OpenPype/pull/3249) - Maya: added jpg to filter for Image Plane Loader [\#3223](https://github.com/pypeclub/OpenPype/pull/3223) +- Maya: added jpg to filter for Image Plane Loader [\#3221](https://github.com/pypeclub/OpenPype/pull/3221) - Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160) ## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19) @@ -134,6 +131,7 @@ **🐛 Bug fixes** - Ftrack: Action delete old versions formatting works [\#3154](https://github.com/pypeclub/OpenPype/pull/3154) +- nuke: adding extract thumbnail settings [\#3148](https://github.com/pypeclub/OpenPype/pull/3148) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 1d8ef28225..e5dfc2bb8f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.1-nightly.2" +__version__ = "3.10.1-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 27b32cf53b..7a620aec8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.1-nightly.2" # OpenPype +version = "3.10.1-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 7b16c6837b1c02defd73fcba984909d085cce61e Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 6 Jun 2022 17:08:37 +0200 Subject: [PATCH 156/194] refacto code to have simpler menu --- openpype/hosts/nuke/api/gizmo_menu.py | 90 ++++++++------ openpype/hosts/nuke/api/lib.py | 109 ++++++++++------ openpype/hosts/nuke/startup/menu.py | 70 +---------- .../defaults/project_settings/nuke.json | 8 +- .../schemas/schema_nuke_scriptsgizmo.json | 117 +++++++++++++++--- 5 files changed, 222 insertions(+), 172 deletions(-) diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py index dd04f4a42e..7f8121372c 100644 --- a/openpype/hosts/nuke/api/gizmo_menu.py +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -1,67 +1,75 @@ import os -import logging +import re import nuke -log = logging.getLogger(__name__) +from openpype.api import Logger + +log = Logger.get_logger(__name__) class GizmoMenu(): - def __init__(self, *args, **kwargs): + def __init__(self, title, icon=None): + + self.toolbar = self._create_toolbar_menu( + title, + icon=icon + ) + self._script_actions = [] - def build_from_configuration(self, parent, configuration): + def _create_toolbar_menu(self, name, icon=None): + nuke_node_menu = nuke.menu("Nodes") + return nuke_node_menu.addMenu( + name, + icon=icon + ) + + def _make_menu_path(self, path, icon=None): + parent = self.toolbar + for folder in re.split(r"/|\\",path): + if not folder: + continue + existing_menu = parent.findItem(folder) + if existing_menu: + parent = existing_menu + else: + parent = parent.addMenu(folder, icon=icon) + + return parent + + def build_from_configuration(self, configuration): for item in configuration: assert isinstance(item, dict), "Configuration is wrong!" - # skip items which have no `type` key - item_type = item.get('type', None) - if not item_type: - log.warning("Missing 'type' from configuration item") - continue + # Construct parent path else parent is toolbar + parent = self.toolbar + gizmo_toolbar_path = item.get("gizmo_toolbar_path") + if gizmo_toolbar_path: + parent = self._make_menu_path(gizmo_toolbar_path) - if item_type == "action": - # filter out `type` from the item dict - config = {key: value for key, value in - item.items() if key != "type"} - - command = str(config['command']) - - icon = config.get('icon', None) - if icon: - try: - icon = icon.format(**os.environ) - except KeyError as e: - log.warning("This environment variable doesn't exist: " - "{}".format(e)) - - hotkey = config.get('hotkey', None) + item_type = item.get("sourcetype") + if item_type == ("python" or "file"): parent.addCommand( - config['title'], - command=command, - icon=icon, - shortcut=hotkey + item['title'], + command=str(item["command"]), + icon=item.get("icon"), + shortcut=item.get('hotkey') ) # add separator # Special behavior for separators - if item_type == "separator": + elif item_type == "separator": parent.addSeparator() # add submenu # items should hold a collection of submenu items (dict) elif item_type == "menu": - assert "items" in item, "Menu is missing 'items' key" - - icon = item.get('icon', None) - if icon: - try: - icon = icon.format(**os.environ) - except KeyError as e: - log.warning("This environment variable doesn't exist: " - "{}".format(e)) - menu = parent.addMenu(item['title'], icon=icon) - self.build_from_configuration(menu, item["items"]) + # assert "items" in item, "Menu is missing 'items' key" + parent.addMenu( + item['title'], + icon=item.get('icon') + ) def add_gizmo_path(self, gizmo_paths): for gizmo_path in gizmo_paths: diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a1ac50ae1a..335e7190a0 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2500,50 +2500,77 @@ def recreate_instance(origin_node, avalon_data=None): return new_node -def find_scripts_gizmo(title, parent): - """ - Check if the menu exists with the given title in the parent - - Args: - title (str): the title name of the scripts menu - - parent (QtWidgets.QMenuBar): the menubar to check - - Returns: - QtWidgets.QMenu or None - - """ - - menu = None - search = [i for i in parent.items() if - isinstance(i, gizmo_menu.GizmoMenu) - and i.title() == title] - - if search: - assert len(search) < 2, ("Multiple instances of menu '{}' " - "in toolbar".format(title)) - menu = search[0] - - return menu - - -def gizmo_creation(title="Gizmos", parent=None, objectName=None, icon=None): +def add_scripts_gizmo(): try: - toolbar = find_scripts_gizmo(title, parent) - if not toolbar: - log.info("Attempting to build toolbar...") - object_name = objectName or title.lower() - toolbar = gizmo_menu.GizmoMenu( - title=title, - parent=parent, - objectName=object_name, - icon=icon - ) - except Exception as e: - log.error(e) + from openpype.hosts.nuke.api import lib + except ImportError: + log.warning( + "Skipping studio.gizmo install, because " + "'scriptsgizmo' module seems unavailable." + ) return - return toolbar + # load configuration of custom menu + project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) + platform_name = platform.system().lower() + + for gizmo_settings in project_settings["nuke"]["gizmo"]: + gizmo_list_definition = gizmo_settings["gizmo_definition"] + print(1, gizmo_list_definition) + toolbar_name = gizmo_settings["toolbar_menu_name"] + # gizmo_toolbar_path = gizmo_settings["gizmo_toolbar_path"] + gizmo_source_dir = gizmo_settings.get( + "gizmo_source_dir", {}).get(platform_name) + toolbar_icon_path = gizmo_settings.get( + "toolbar_icon_path", {}).get(platform_name) + + if not gizmo_source_dir: + log.debug("Skipping studio gizmo `{}`, no gizmo path found.".format( + toolbar_name + )) + return + + if not gizmo_list_definition: + log.debug("Skipping studio gizmo `{}`, no definition found.".format( + toolbar_name + )) + return + + if toolbar_icon_path: + try: + toolbar_icon_path = toolbar_icon_path.format(**os.environ) + except KeyError as e: + log.error( + "This environment variable doesn't exist: {}".format(e) + ) + + existing_gizmo_path = [] + for source_dir in gizmo_source_dir: + try: + resolve_source_dir = source_dir.format(**os.environ) + except KeyError as e: + log.error( + "This environment variable doesn't exist: {}".format(e) + ) + continue + if not os.path.exists(resolve_source_dir): + log.warning( + "The source of gizmo `{}` does not exists".format( + resolve_source_dir + ) + ) + continue + existing_gizmo_path.append(resolve_source_dir) + + # run the launcher for Nuke toolbar + toolbar_menu = gizmo_menu.GizmoMenu( + title=toolbar_name, + icon=toolbar_icon_path + ) + + # apply configuration + toolbar_menu.add_gizmo_path(existing_gizmo_path) + toolbar_menu.build_from_configuration(gizmo_list_definition) class NukeDirmap(HostDirmap): diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 715bab8ea5..1461d41385 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -8,7 +8,8 @@ from openpype.hosts.nuke.api.lib import ( on_script_load, check_inventory_versions, WorkfileSettings, - dirmap_file_name_filter + dirmap_file_name_filter, + add_scripts_gizmo ) from openpype.settings import get_project_settings @@ -60,71 +61,4 @@ def add_scripts_menu(): add_scripts_menu() - -def add_scripts_gizmo(): - try: - from openpype.hosts.nuke.api import lib - except ImportError: - log.warning( - "Skipping studio.gizmo install, because " - "'scriptsgizmo' module seems unavailable." - ) - return - - # load configuration of custom menu - project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) - - for gizmo in project_settings["nuke"]["gizmo"]: - config = gizmo["gizmo_definition"] - toolbar_name = gizmo["toolbar_menu_name"] - gizmo_path = gizmo["gizmo_path"] - icon = gizmo['toolbar_icon_path'] - - if not any(gizmo_path): - log.warning("Skipping studio gizmo, no gizmo path found.") - return - - if not config: - log.warning("Skipping studio gizmo, no definition found.") - return - - try: - icon = icon.format(**os.environ) - except KeyError as e: - log.warning( - "This environment variable doesn't exist: {}".format(e) - ) - - existing_gizmo_path = [] - for gizmo in gizmo_path: - try: - gizmo = gizmo.format(**os.environ) - except KeyError as e: - log.warning( - "This environment variable doesn't exist: {}".format(e) - ) - continue - if not os.path.exists(gizmo): - log.warning( - "The source of gizmo `{}` does not exists".format(gizmo) - ) - continue - existing_gizmo_path.append(gizmo) - - nuke_toolbar = nuke.menu("Nodes") - toolbar = nuke_toolbar.addMenu(toolbar_name, icon=icon) - - # run the launcher for Nuke toolbar - studio_menu = lib.gizmo_creation( - title=toolbar_name, - parent=toolbar, - objectName=toolbar_name.lower().replace(" ", "_"), - icon=icon - ) - - # apply configuration - studio_menu.add_gizmo_path(existing_gizmo_path) - studio_menu.build_from_configuration(toolbar, config) - - add_scripts_gizmo() diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 6c6454de36..63978ad1be 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -293,8 +293,12 @@ "gizmo": [ { "toolbar_menu_name": "OpenPype Gizmo", - "toolbar_icon_path": "path/to/nuke/icon.png", - "gizmo_path": ["path/to/nuke/gizmo"], + "gizmo_path": { + "windows": [], + "darwin": [], + "linux": [] + }, + "toolbar_icon_path": {}, "gizmo_definition": [ { "type": "action", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json index c1e67842ce..80fda56175 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json @@ -14,28 +14,105 @@ }, { "type": "path", - "key": "toolbar_icon_path", - "label": "Toolbar Icon Path", - "multipath": false + "key": "gizmo_source_dir", + "label": "Gizmo directory path", + "multipath": true, + "multiplatform": true }, { - "type": "splitter" - }, - { - "type": "label", - "label": "Absolute path to gizmo folders." - }, - { - "type": "path", - "key": "gizmo_path", - "label": "Gizmo Path", - "multipath": true - }, - { - "type": "raw-json", - "key": "gizmo_definition", - "label": "Gizmo definition", - "is_list": true + "type": "collapsible-wrap", + "label": "Options", + "collapsible": true, + "collapsed": true, + "children": [ + { + "type": "path", + "key": "toolbar_icon_path", + "label": "Toolbar Icon Path", + "multipath": false, + "multiplatform": true + }, + { + "type": "splitter" + }, + { + "type": "list", + "key": "gizmo_definition", + "label": "Gizmo definitions", + "use_label_wrap": true, + "object_type": { + "type": "dict-conditional", + "enum_key": "sourcetype", + "enum_label": "Type of usage", + "enum_children": [ + { + "key": "python", + "label": "Python", + "children": [ + { + "type": "text", + "key": "title", + "label": "Title" + }, + { + "type": "text", + "key": "gizmo_toolbar_path", + "label": "Toolbar path" + }, + { + "type": "text", + "key": "command", + "label": "Python command" + }, + { + "type": "text", + "key": "shortcut", + "label": "Hotkey" + } + ] + }, + { + "key": "file", + "label": "File", + "children": [ + { + "type": "text", + "key": "title", + "label": "Title" + }, + { + "type": "text", + "key": "gizmo_toolbar_path", + "label": "Toolbar path" + }, + { + "type": "text", + "key": "file_name", + "label": "Gizmo file name" + }, + { + "type": "text", + "key": "shortcut", + "label": "Hotkey" + } + + ] + }, + { + "key": "separator", + "label": "Separator", + "children": [ + { + "type": "text", + "key": "gizmo_toolbar_path", + "label": "Toolbar path" + } + ] + } + ] + } + } + ] } ] } From b77cb4ba1a367e5974342e670241d19137fb2a3e Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 6 Jun 2022 19:17:34 +0200 Subject: [PATCH 157/194] Add global menu from settings --- openpype/hosts/nuke/api/gizmo_menu.py | 52 +++---- openpype/hosts/nuke/api/lib.py | 1 - .../defaults/project_settings/nuke.json | 13 +- .../schemas/schema_nuke_scriptsgizmo.json | 133 +++++++++--------- 4 files changed, 106 insertions(+), 93 deletions(-) diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py index 7f8121372c..42b5812360 100644 --- a/openpype/hosts/nuke/api/gizmo_menu.py +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -38,38 +38,42 @@ class GizmoMenu(): return parent def build_from_configuration(self, configuration): - for item in configuration: - assert isinstance(item, dict), "Configuration is wrong!" - + for menu in configuration: # Construct parent path else parent is toolbar parent = self.toolbar - gizmo_toolbar_path = item.get("gizmo_toolbar_path") + gizmo_toolbar_path = menu.get("gizmo_toolbar_path") if gizmo_toolbar_path: parent = self._make_menu_path(gizmo_toolbar_path) - item_type = item.get("sourcetype") + for item in menu["sub_gizmo_list"]: + assert isinstance(item, dict), "Configuration is wrong!" - if item_type == ("python" or "file"): - parent.addCommand( - item['title'], - command=str(item["command"]), - icon=item.get("icon"), - shortcut=item.get('hotkey') - ) + if not item.get("title"): + continue - # add separator - # Special behavior for separators - elif item_type == "separator": - parent.addSeparator() + item_type = item.get("sourcetype") - # add submenu - # items should hold a collection of submenu items (dict) - elif item_type == "menu": - # assert "items" in item, "Menu is missing 'items' key" - parent.addMenu( - item['title'], - icon=item.get('icon') - ) + if item_type == ("python" or "file"): + parent.addCommand( + item["title"], + command=str(item["command"]), + icon=item.get("icon"), + shortcut=item.get("hotkey") + ) + + # add separator + # Special behavior for separators + elif item_type == "separator": + parent.addSeparator() + + # add submenu + # items should hold a collection of submenu items (dict) + elif item_type == "menu": + # assert "items" in item, "Menu is missing 'items' key" + parent.addMenu( + item['title'], + icon=item.get('icon') + ) def add_gizmo_path(self, gizmo_paths): for gizmo_path in gizmo_paths: diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 335e7190a0..0d766c8459 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2516,7 +2516,6 @@ def add_scripts_gizmo(): for gizmo_settings in project_settings["nuke"]["gizmo"]: gizmo_list_definition = gizmo_settings["gizmo_definition"] - print(1, gizmo_list_definition) toolbar_name = gizmo_settings["toolbar_menu_name"] # gizmo_toolbar_path = gizmo_settings["gizmo_toolbar_path"] gizmo_source_dir = gizmo_settings.get( diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 63978ad1be..c609a0927a 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -301,10 +301,15 @@ "toolbar_icon_path": {}, "gizmo_definition": [ { - "type": "action", - "sourcetype": "python", - "title": "Gizmo Note", - "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')" + "gizmo_toolbar_path": "/path/to/menu", + "sub_gizmo_list": [ + { + "sourcetype": "python", + "title": "Gizmo Note", + "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')", + "shortcut": "" + } + ] } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json index 80fda56175..abe14970c5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_scriptsgizmo.json @@ -41,73 +41,78 @@ "label": "Gizmo definitions", "use_label_wrap": true, "object_type": { - "type": "dict-conditional", - "enum_key": "sourcetype", - "enum_label": "Type of usage", - "enum_children": [ + "type": "dict", + "children": [ { - "key": "python", - "label": "Python", - "children": [ - { - "type": "text", - "key": "title", - "label": "Title" - }, - { - "type": "text", - "key": "gizmo_toolbar_path", - "label": "Toolbar path" - }, - { - "type": "text", - "key": "command", - "label": "Python command" - }, - { - "type": "text", - "key": "shortcut", - "label": "Hotkey" - } - ] + "type": "text", + "key": "gizmo_toolbar_path", + "label": "Gizmo Menu Path" }, { - "key": "file", - "label": "File", - "children": [ - { - "type": "text", - "key": "title", - "label": "Title" - }, - { - "type": "text", - "key": "gizmo_toolbar_path", - "label": "Toolbar path" - }, - { - "type": "text", - "key": "file_name", - "label": "Gizmo file name" - }, - { - "type": "text", - "key": "shortcut", - "label": "Hotkey" - } - - ] - }, - { - "key": "separator", - "label": "Separator", - "children": [ - { - "type": "text", - "key": "gizmo_toolbar_path", - "label": "Toolbar path" - } - ] + "type": "list", + "key": "sub_gizmo_list", + "label": "Sub Gizmo List", + "use_label_wrap": true, + "object_type": { + "type": "dict-conditional", + "enum_key": "sourcetype", + "enum_label": "Type of usage", + "enum_children": [ + { + "key": "python", + "label": "Python", + "children": [ + { + "type": "text", + "key": "title", + "label": "Title" + }, + { + "type": "text", + "key": "command", + "label": "Python command" + }, + { + "type": "text", + "key": "shortcut", + "label": "Hotkey" + } + ] + }, + { + "key": "file", + "label": "File", + "children": [ + { + "type": "text", + "key": "title", + "label": "Title" + }, + { + "type": "text", + "key": "file_name", + "label": "Gizmo file name" + }, + { + "type": "text", + "key": "shortcut", + "label": "Hotkey" + } + ] + }, + { + "key": "separator", + "label": "Separator", + "children": [ + { + "type": "text", + "key": "gizmo_toolbar_path", + "label": "Toolbar path" + } + ] + } + ] + } } ] } From 25e9ee617e798e14cffb6e2090e1a4dbdb88214c Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Mon, 6 Jun 2022 21:00:24 +0200 Subject: [PATCH 158/194] linter correction --- openpype/hosts/nuke/api/gizmo_menu.py | 2 +- openpype/hosts/nuke/api/lib.py | 20 ++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/nuke/api/gizmo_menu.py b/openpype/hosts/nuke/api/gizmo_menu.py index 42b5812360..0f1a3e03fc 100644 --- a/openpype/hosts/nuke/api/gizmo_menu.py +++ b/openpype/hosts/nuke/api/gizmo_menu.py @@ -26,7 +26,7 @@ class GizmoMenu(): def _make_menu_path(self, path, icon=None): parent = self.toolbar - for folder in re.split(r"/|\\",path): + for folder in re.split(r"/|\\", path): if not folder: continue existing_menu = parent.findItem(folder) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0d766c8459..2c5989309b 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2501,14 +2501,6 @@ def recreate_instance(origin_node, avalon_data=None): def add_scripts_gizmo(): - try: - from openpype.hosts.nuke.api import lib - except ImportError: - log.warning( - "Skipping studio.gizmo install, because " - "'scriptsgizmo' module seems unavailable." - ) - return # load configuration of custom menu project_settings = get_project_settings(os.getenv("AVALON_PROJECT")) @@ -2524,15 +2516,15 @@ def add_scripts_gizmo(): "toolbar_icon_path", {}).get(platform_name) if not gizmo_source_dir: - log.debug("Skipping studio gizmo `{}`, no gizmo path found.".format( - toolbar_name - )) + log.debug("Skipping studio gizmo `{}`, " + "no gizmo path found.".format(toolbar_name) + ) return if not gizmo_list_definition: - log.debug("Skipping studio gizmo `{}`, no definition found.".format( - toolbar_name - )) + log.debug("Skipping studio gizmo `{}`, " + "no definition found.".format(toolbar_name) + ) return if toolbar_icon_path: From e2d6d903e99120048d92cb7fbabe42b111df373c Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:23:45 +0000 Subject: [PATCH 159/194] docs: update README.md [skip ci] --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b6966adbc4..b8c04f8b49 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-26-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) OpenPype ==== @@ -328,6 +328,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Malthaldar

💻
Sven Neve

💻
zafrs

💻 +
Félix David

💻 📖 From 884fce81ce10c6f31e8738b1e7c6f178787ba9df Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Tue, 7 Jun 2022 11:23:46 +0000 Subject: [PATCH 160/194] docs: update .all-contributorsrc [skip ci] --- .all-contributorsrc | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index a3b85cae68..b30f3b2499 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -309,7 +309,18 @@ "contributions": [ "code" ] + }, + { + "login": "Tilix4", + "name": "Félix David", + "avatar_url": "https://avatars.githubusercontent.com/u/22875539?v=4", + "profile": "http://felixdavid.com/", + "contributions": [ + "code", + "doc" + ] } ], - "contributorsPerLine": 7 -} \ No newline at end of file + "contributorsPerLine": 7, + "skipCi": true +} From 1819c66e7bed0b3e9af91cf1d408bd91f8adb7b2 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 8 Jun 2022 03:51:00 +0000 Subject: [PATCH 161/194] [Automated] Bump version --- CHANGELOG.md | 42 +++++++++++++----------------------------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50cb1d423e..1d7798cb48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,28 @@ # Changelog -## [3.10.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.11.0-nightly.1](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.10.0...HEAD) +### 📖 Documentation + +- doc: adding royal render and multiverse to the web site [\#3285](https://github.com/pypeclub/OpenPype/pull/3285) + **🚀 Enhancements** - General: Updated windows oiio tool [\#3268](https://github.com/pypeclub/OpenPype/pull/3268) +- Unreal: add support for skeletalMesh and staticMesh to loaders [\#3267](https://github.com/pypeclub/OpenPype/pull/3267) - Maya: reference loaders could store placeholder in referenced url [\#3264](https://github.com/pypeclub/OpenPype/pull/3264) - TVPaint: Init file for TVPaint worker also handle guideline images [\#3250](https://github.com/pypeclub/OpenPype/pull/3250) - Nuke: Change default icon path in settings [\#3247](https://github.com/pypeclub/OpenPype/pull/3247) **🐛 Bug fixes** +- Global: extract review slate issues [\#3286](https://github.com/pypeclub/OpenPype/pull/3286) - Webpublisher: return only active projects in ProjectsEndpoint [\#3281](https://github.com/pypeclub/OpenPype/pull/3281) +- Hiero: add support for task tags 3.10.x [\#3279](https://github.com/pypeclub/OpenPype/pull/3279) +- General: Fix Oiio tool path resolving [\#3278](https://github.com/pypeclub/OpenPype/pull/3278) +- Maya: Fix udim support for e.g. uppercase \ tag [\#3266](https://github.com/pypeclub/OpenPype/pull/3266) - Nuke: bake reformat was failing on string type [\#3261](https://github.com/pypeclub/OpenPype/pull/3261) - Maya: hotfix Pxr multitexture in looks [\#3260](https://github.com/pypeclub/OpenPype/pull/3260) - Unreal: Fix Camera Loading if Layout is missing [\#3255](https://github.com/pypeclub/OpenPype/pull/3255) @@ -21,13 +30,14 @@ - Unreal: Fixed Render creation in UE5 [\#3239](https://github.com/pypeclub/OpenPype/pull/3239) - Unreal: Fixed Camera loading in UE5 [\#3238](https://github.com/pypeclub/OpenPype/pull/3238) - Flame: debugging [\#3224](https://github.com/pypeclub/OpenPype/pull/3224) -- Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - add silent audio to slate [\#3162](https://github.com/pypeclub/OpenPype/pull/3162) **Merged pull requests:** - Maya: better handling of legacy review subsets names [\#3269](https://github.com/pypeclub/OpenPype/pull/3269) +- Deadline: publishing of animation and pointcache on a farm [\#3225](https://github.com/pypeclub/OpenPype/pull/3225) - Nuke: add pointcache and animation to loader [\#3186](https://github.com/pypeclub/OpenPype/pull/3186) +- Add a gizmo menu to nuke [\#3172](https://github.com/pypeclub/OpenPype/pull/3172) ## [3.10.0](https://github.com/pypeclub/OpenPype/tree/3.10.0) (2022-05-26) @@ -37,7 +47,6 @@ - General: OpenPype modules publish plugins are registered in host [\#3180](https://github.com/pypeclub/OpenPype/pull/3180) - General: Creator plugins from addons can be registered [\#3179](https://github.com/pypeclub/OpenPype/pull/3179) -- Ftrack: Single image reviewable [\#3157](https://github.com/pypeclub/OpenPype/pull/3157) **🚀 Enhancements** @@ -50,10 +59,6 @@ - Maya: added clean\_import option to Import loader [\#3181](https://github.com/pypeclub/OpenPype/pull/3181) - Add the scripts menu definition to nuke [\#3168](https://github.com/pypeclub/OpenPype/pull/3168) - Maya: add maya 2023 to default applications [\#3167](https://github.com/pypeclub/OpenPype/pull/3167) -- Compressed bgeo publishing in SAP and Houdini loader [\#3153](https://github.com/pypeclub/OpenPype/pull/3153) -- General: Add 'dataclasses' to required python modules [\#3149](https://github.com/pypeclub/OpenPype/pull/3149) -- Hooks: Tweak logging grammar [\#3147](https://github.com/pypeclub/OpenPype/pull/3147) -- Nuke: settings for reformat node in CreateWriteRender node [\#3143](https://github.com/pypeclub/OpenPype/pull/3143) **🐛 Bug fixes** @@ -66,6 +71,7 @@ - Hiero: debugging frame range and other 3.10 [\#3222](https://github.com/pypeclub/OpenPype/pull/3222) - Project Manager: Fix persistent editors on project change [\#3218](https://github.com/pypeclub/OpenPype/pull/3218) - Deadline: instance data overwrite fix [\#3214](https://github.com/pypeclub/OpenPype/pull/3214) +- Ftrack: Push hierarchical attributes action works [\#3210](https://github.com/pypeclub/OpenPype/pull/3210) - Standalone Publisher: Always create new representation for thumbnail [\#3203](https://github.com/pypeclub/OpenPype/pull/3203) - Photoshop: skip collector when automatic testing [\#3202](https://github.com/pypeclub/OpenPype/pull/3202) - Nuke: render/workfile version sync doesn't work on farm [\#3185](https://github.com/pypeclub/OpenPype/pull/3185) @@ -76,9 +82,6 @@ - General: Oiio conversion for ffmpeg checks for invalid characters [\#3166](https://github.com/pypeclub/OpenPype/pull/3166) - Fix for attaching render to subset [\#3164](https://github.com/pypeclub/OpenPype/pull/3164) - Harmony: fixed missing task name in render instance [\#3163](https://github.com/pypeclub/OpenPype/pull/3163) -- Ftrack: Action delete old versions formatting works [\#3152](https://github.com/pypeclub/OpenPype/pull/3152) -- Deadline: fix the output directory [\#3144](https://github.com/pypeclub/OpenPype/pull/3144) -- General: New Session schema [\#3141](https://github.com/pypeclub/OpenPype/pull/3141) **🔀 Refactored code** @@ -89,7 +92,6 @@ - Harmony: message length in 21.1 [\#3257](https://github.com/pypeclub/OpenPype/pull/3257) - Harmony: 21.1 fix [\#3249](https://github.com/pypeclub/OpenPype/pull/3249) - Maya: added jpg to filter for Image Plane Loader [\#3223](https://github.com/pypeclub/OpenPype/pull/3223) -- Maya: added jpg to filter for Image Plane Loader [\#3221](https://github.com/pypeclub/OpenPype/pull/3221) - Webpublisher: replace space by underscore in subset names [\#3160](https://github.com/pypeclub/OpenPype/pull/3160) ## [3.9.8](https://github.com/pypeclub/OpenPype/tree/3.9.8) (2022-05-19) @@ -119,24 +121,6 @@ [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.6...3.9.7) -**🆕 New features** - -- Ftrack: Single image reviewable [\#3158](https://github.com/pypeclub/OpenPype/pull/3158) - -**🚀 Enhancements** - -- Deadline output dir issue to 3.9x [\#3155](https://github.com/pypeclub/OpenPype/pull/3155) -- nuke: removing redundant code from startup [\#3142](https://github.com/pypeclub/OpenPype/pull/3142) - -**🐛 Bug fixes** - -- Ftrack: Action delete old versions formatting works [\#3154](https://github.com/pypeclub/OpenPype/pull/3154) -- nuke: adding extract thumbnail settings [\#3148](https://github.com/pypeclub/OpenPype/pull/3148) - -**Merged pull requests:** - -- Webpublisher: replace space by underscore in subset names [\#3159](https://github.com/pypeclub/OpenPype/pull/3159) - ## [3.9.6](https://github.com/pypeclub/OpenPype/tree/3.9.6) (2022-05-03) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.9.5...3.9.6) diff --git a/openpype/version.py b/openpype/version.py index e5dfc2bb8f..4c78a6e0a1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.10.1-nightly.3" +__version__ = "3.11.0-nightly.1" diff --git a/pyproject.toml b/pyproject.toml index 5649ff2073..362f6a62d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.10.1-nightly.3" # OpenPype +version = "3.11.0-nightly.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 24cb024dd9d27e5c68fcd6f7d9097297031d7845 Mon Sep 17 00:00:00 2001 From: Felix Wang Date: Wed, 8 Jun 2022 00:45:15 -0700 Subject: [PATCH 162/194] Maya: Look assigner UI improvements (#3208) Co-authored-by: felix.wang --- openpype/hosts/maya/api/lib.py | 5 ++++- openpype/tools/utils/host_tools.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 088304ab05..bce03a648b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1737,8 +1737,11 @@ def apply_shaders(relationships, shadernodes, nodes): log.warning("No nodes found for shading engine " "'{0}'".format(id_shading_engines[0])) continue + try: + cmds.sets(filtered_nodes, forceElement=id_shading_engines[0]) + except RuntimeError as rte: + log.error("Error during shader assignment: {}".format(rte)) - cmds.sets(filtered_nodes, forceElement=id_shading_engines[0]) # endregion apply_attributes(attributes, nodes_by_id) diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index d8f4570120..9dbbe25fda 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -105,6 +105,7 @@ class HostToolsHelper: loader_tool.show() loader_tool.raise_() loader_tool.activateWindow() + loader_tool.showNormal() if use_context is None: use_context = False @@ -180,6 +181,7 @@ class HostToolsHelper: # Pull window to the front. scene_inventory_tool.raise_() scene_inventory_tool.activateWindow() + scene_inventory_tool.showNormal() def get_library_loader_tool(self, parent): """Create, cache and return library loader tool window.""" @@ -200,8 +202,10 @@ class HostToolsHelper: library_loader_tool.show() library_loader_tool.raise_() library_loader_tool.activateWindow() + library_loader_tool.showNormal() library_loader_tool.refresh() + def show_publish(self, parent=None): """Try showing the most desirable publish GUI @@ -243,6 +247,11 @@ class HostToolsHelper: look_assigner_tool = self.get_look_assigner_tool(parent) look_assigner_tool.show() + # Pull window to the front. + look_assigner_tool.raise_() + look_assigner_tool.activateWindow() + look_assigner_tool.showNormal() + def get_experimental_tools_dialog(self, parent=None): """Dialog of experimental tools. @@ -270,6 +279,7 @@ class HostToolsHelper: dialog.show() dialog.raise_() dialog.activateWindow() + dialog.showNormal() def get_tool_by_name(self, tool_name, parent=None, *args, **kwargs): """Show tool by it's name. From 964504eb7706eaf2f0ac9013f31a72192c840845 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Wed, 8 Jun 2022 10:23:43 +0200 Subject: [PATCH 163/194] add app key to documentation --- website/docs/admin_settings_project_anatomy.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/admin_settings_project_anatomy.md b/website/docs/admin_settings_project_anatomy.md index b98819cd8a..6e0b49f152 100644 --- a/website/docs/admin_settings_project_anatomy.md +++ b/website/docs/admin_settings_project_anatomy.md @@ -67,6 +67,7 @@ We have a few required anatomy templates for OpenPype to work properly, however | `ext` | File extension | | `representation` | Representation name | | `frame` | Frame number for sequence files. | +| `app` | Application Name | | `output` | | | `comment` | | From aa7e7093df8d72357118bdb34dbe03e4e73d6801 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 164/194] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From f50999d0927bf533a74417479e4cdb4a06b32b3d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 165/194] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From eba5691a2db74da0ff994299d9c252f03dcea021 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Jun 2022 13:23:14 +0200 Subject: [PATCH 166/194] use width for width and height for height in maya render --- openpype/hosts/maya/plugins/publish/collect_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index fbd2e81279..8b911a867d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -340,10 +340,10 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "expectedFiles": full_exp_files, "publishRenderMetadataFolder": common_publish_meta_path, "resolutionWidth": lib.get_attr_in_layer( - "defaultResolution.height", layer=layer_name + "defaultResolution.width", layer=layer_name ), "resolutionHeight": lib.get_attr_in_layer( - "defaultResolution.width", layer=layer_name + "defaultResolution.height", layer=layer_name ), "pixelAspect": lib.get_attr_in_layer( "defaultResolution.pixelAspect", layer=layer_name From a45795526698b9a668df876a01ce002a5a05024f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 8 Jun 2022 13:33:26 +0200 Subject: [PATCH 167/194] add missing default settings for nuke gizmo --- .../defaults/project_settings/nuke.json | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c609a0927a..16348bec85 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -27,6 +27,34 @@ } ] }, + "gizmo": [ + { + "toolbar_menu_name": "OpenPype Gizmo", + "gizmo_source_dir": { + "windows": [], + "darwin": [], + "linux": [] + }, + "toolbar_icon_path": { + "windows": "", + "darwin": "", + "linux": "" + }, + "gizmo_definition": [ + { + "gizmo_toolbar_path": "/path/to/menu", + "sub_gizmo_list": [ + { + "sourcetype": "python", + "title": "Gizmo Note", + "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')", + "shortcut": "" + } + ] + } + ] + } + ], "create": { "CreateWriteRender": { "fpath_template": "{work}/renders/nuke/{subset}/{subset}.{frame}.{ext}", @@ -290,29 +318,5 @@ } ] }, - "gizmo": [ - { - "toolbar_menu_name": "OpenPype Gizmo", - "gizmo_path": { - "windows": [], - "darwin": [], - "linux": [] - }, - "toolbar_icon_path": {}, - "gizmo_definition": [ - { - "gizmo_toolbar_path": "/path/to/menu", - "sub_gizmo_list": [ - { - "sourcetype": "python", - "title": "Gizmo Note", - "command": "nuke.nodes.StickyNote(label='You can create your own toolbar menu in the Nuke GizmoMenu of OpenPype')", - "shortcut": "" - } - ] - } - ] - } - ], "filters": {} -} +} \ No newline at end of file From edb55949df619a81c1828571030634a4b0c49584 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 168/194] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 15e51cd6a640aea61eb927b84ce6b48990d206f3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 169/194] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index efd22e13c8..2e0e30b74b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From c8c36144cb26df5d0024fcd02df265736bbd209f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 170/194] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 770b6d3ab2ee9e3bdf460cee4fdba96d67e44fb2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 171/194] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_entities (list): Asset entities for which want to find data + + Returns: + (dict): collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: return # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From a5a3685f2b5b99bbd1f8de78581eb17af0175ed3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 172/194] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From f2ae0ffa5950d922fd3cb90ce8bbf30ec64ca0b7 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 173/194] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From 41a47bb2bfb9b728f0cad37f5614e5d382b2d9d1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 174/194] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From 58814d21e4688fbb13d183fab7ba9010c68b57f8 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 175/194] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 6cb037d3d63290752bacc0aa8c2b81cac8e3b370 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 176/194] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From aa88ee13c0d3b647dc9c11534ed4a742168b0e1d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 177/194] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From d8edf2b1aa9e83861bad1b8aef6da69cc6011de4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 178/194] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From bae9eef400e2b1195d8ece023543ca8d89c83b1b Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 179/194] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From 349d57a4a8ec86364d64b02798c7579f6c3cb5c2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 180/194] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From e2506d569adf78c74ba6452643fec5d1afca0ab2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 181/194] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From a6d948aa93e2c84c001a109c50dede9b9c160321 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 182/194] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From d6543bf281a418fe768059a58a1a9eb8257ef68f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 183/194] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From 4a02dd039de637398aa6c2a1d2c26ba772f720da Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 9 Jun 2022 14:51:48 +0200 Subject: [PATCH 184/194] set empty placholder parent at creation --- openpype/hosts/maya/api/lib_template_builder.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 108988a676..20f6f041fb 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,8 +69,7 @@ def create_placeholder(): defaultValue=-1 ) - parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', "", type="string") def create_placeholder_name(args, options): @@ -131,11 +130,11 @@ def update_placeholder(): defaultValue=-1 ) - selected = cmds.ls(selection=True, long=True) + """selected = cmds.ls(selection=True, long=True) selected = selected[0].split('|')[-2] selected = cmds.ls(selected) - parents = cmds.ls(selected, long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + parents = cmds.ls(selected, long=True)""" + cmds.setAttr(placeholder + '.parent', '', type="string") def create_options(args): From a97f5379b16f4141035629aa23d7ca26f16fdced Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:17:30 +0200 Subject: [PATCH 185/194] Add documentation --- website/docs/admin_hosts_maya.md | 50 ++++++++++++++++++ .../docs/assets/maya-create_placeholder.png | Bin 0 -> 31543 bytes website/docs/assets/maya-placeholder_new.png | Bin 0 -> 28008 bytes 3 files changed, 50 insertions(+) create mode 100644 website/docs/assets/maya-create_placeholder.png create mode 100644 website/docs/assets/maya-placeholder_new.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 93bf32798f..c55dcc1b36 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -120,3 +120,53 @@ raw json. You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping** ![Dirmap settings](assets/maya-admin_dirmap_settings.png) + +## Templated Build Workfile + +Building a workfile using a template designed by users. Helping to assert homogeneous subsets hierarchy and imports. Template stored as file easy to define, change and customize for production needs. + + **1. Make a template** + +Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. + +![Dirmap settings](assets/maya-workfile-outliner.png) + +If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** + +![create placeholder](assets/maya-create_placeholder.png) + +- **Configure placeholders** + +Fill in the necessary fields (the optional fields are regex filters) + +![new place holder](assets/maya-placeholder_new.png) + + + - Builder type: Wether the the placeholder should load current asset representations or linked assets representations + + - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + + - Family: Family of the representation to load (main, look, image, etc ...) + + - Loader: Placeholder loader name that will be used to load corresponding representations + + - Order: Priority for current placeholder loader (priority is lowest first, highet last) + +- **Save your template** + + + **2. Configure Template** + +- **Go to Studio settings > Project > Your DCC > Templated Build Settings** +- Add a profile for your task and enter path to your template +![Dirmap settings](assets/settings/template_build_workfile.png) + +**3. Build your workfile** + +- Open maya + +- Build your workfile + +![Dirmap settings](assets/maya-build_workfile_from_template.png) + + diff --git a/website/docs/assets/maya-create_placeholder.png b/website/docs/assets/maya-create_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..3f49fe2e2b801e89e2027ec521dbf700b97f80d2 GIT binary patch literal 31543 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 Pn$XtJKYU;9ROtTzYS3%b literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-placeholder_new.png b/website/docs/assets/maya-placeholder_new.png new file mode 100644 index 0000000000000000000000000000000000000000..106a5275cdb5bc0588c811d9a059ec3b6e072e20 GIT binary patch literal 28008 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 literal 0 HcmV?d00001 From e4a7dd6548eec1c3d85bdf133c4f273432313c12 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:27:00 +0200 Subject: [PATCH 186/194] add sreenshot --- .../maya-build_workfile_from_template.png | Bin 0 -> 20676 bytes .../assets/settings/template_build_workfile.png | Bin 0 -> 29814 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/maya-build_workfile_from_template.png create mode 100644 website/docs/assets/settings/template_build_workfile.png diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png new file mode 100644 index 0000000000000000000000000000000000000000..336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8 GIT binary patch literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef87861fe97322a3b23c28ee3644b76ce995e9d GIT binary patch literal 29814 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 literal 0 HcmV?d00001 From 4441a7cc1c07dc62fc04bd7428bd2f7efcdb0fd9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:50:42 +0200 Subject: [PATCH 187/194] add screenshot --- website/docs/admin_hosts_maya.md | 5 +++-- .../maya-build_workfile_from_template.png | Bin 20676 -> 29814 bytes .../docs/assets/maya-workfile-outliner.png | Bin 0 -> 4835 bytes .../settings/template_build_workfile.png | Bin 29814 -> 12596 bytes 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 website/docs/assets/maya-workfile-outliner.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index c55dcc1b36..0ba030c26f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -129,7 +129,7 @@ Building a workfile using a template designed by users. Helping to assert homoge Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. -![Dirmap settings](assets/maya-workfile-outliner.png) +![maya outliner](assets/maya-workfile-outliner.png) If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** @@ -159,7 +159,8 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![Dirmap settings](assets/settings/template_build_workfile.png) + +![build template](assets/settings/template_build_workfile.png) **3. Build your workfile** diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png index 336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8..7ef87861fe97322a3b23c28ee3644b76ce995e9d 100644 GIT binary patch literal 29814 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 literal 20676 zcmagGWmuG3_c(k{&?z9Df&!A#Ie-YLD4?i_LyU)(?uG#blvY|A6i_JvhlT+}kQ%y6 zx?^DI_z#}vJm>j+dEXCQ*Kp6i*IsL{wO6lstD~hsLCQ=D0070kyLTP~00@o$Qza(E zKPd!F5dZ)uaPN-F6A#cvnso`=VElCP(!0+OzC6~I+~pFF<*&JMD=M6%d4w^-=Z_A5te*wY$NV`XBM6I;SRgS4M(5Z zd)LqB?_;?>%kJKazHgRyUgM3^uft58$>p86W$ZRQHS|94D>!>m=U3-;pjd}EY-O4u zcOaC3qz1^Q1_aBpiidXo^8};{JSM?^1L_7W5W*HnAhpA6DO2|E2T0%}<)8OJ-Drbu z03?&x(XHZq2?)O&beHWPs8x8VEiG!zLC(r-nUp$2`HOG}yEI)}AN2giStT zlj7}8m+0`uO@@j*T;ewR7y-8+T?*Cau@1q>SJk?!3zK&)-R0n(bQ%8Cd0N!TPbB0m?V?8?l{Ta<>a!(^)=(IRn{xmD`jh5dzp;cc_HsY|Y8asPYq(GOSqRZxwd*_4Ma z2rjI|to$5Ji14WhH8L&mo$~gMbk`aQoe82&6id@^S{o9FU8b>&8fNm0rvx~fj@-N2 zlgn_}u}x%4}176k~?wkB#!jlON}e`bm9p1ZPP zD8)<5w}|65Vg0hjT`a~U0=5@Z_xQw{*k!a67GLl&A{bTd>z+O<)vDmMLpdp{Bc_|~ z^FqsI7hpOjY_d&Rnj0osdhJ~5C0F8b_(PwT=^YaEpfYkzrG+|hv58*9;ainnle z7TYvlFi`g@*?_TK4OqF%}DO* zdr5_bf4NV2!2E2TwW?vfWSf%?y&x;G;zF5_<&KB1QrdY9yiPG)o0oY^$keA$Dx zN{~HW?ig3Y$$A#|4zskxUvP;zjLsy=(`fy}Zw1Hn54`~dwevAHJ8>g!_f$nFLV7es zS1jBt=7fGRMA+z0nP)Jl!$0MeUKp{wnh$ z!2+%D+Rz=z9y4l=!dS`jA^8tQ?;}_oD(v;o96Ih#@5xMjv-lN`bejD^@+m`qL3Kk@ ziO1!H);8_NuCW!_6Vn{3F_So#kd~sW-Ly-e_1T_3FLysIb$xOw8=}lfr%p{*#XWOp zfg9m_#4|;&#?Z?X%hQAV9N=YtWWQUW6s{g}wa2R$DZjDMsV{FJAk| zj*S%MVzp~HMzKE?C)cq@yOkfDTTxZ8fdJGBnj9iJcAsrmm;_C7o@YM>kp2osiMJ|F z>Uc6IpqS|QL*+&Z*q&bpjw|Ri*#DBo)|8;vxhbx=BIT6Xw%72AwDFlhe0%YeREBOc z{KFmswtRB*LH$8p2+P!ffldC%=i4?CM`_S%<=(6Zhulv9=QNP{g9fwXU?F^t(N>HLUHA8qAud0DQNWtYR z6g~{(3MWa0c~AtwoQ6H$21VDL4}eUOvK32_wwFpY^7^c3X;T_Wu@t)7FU7_QkSKd0 zuetz$L791^euH0Knv!yfEraxl>WmIisVm|d06Uxsdb?w)fDMzO+Py^u%p5Ic*iYeU z(I;+V;enP#QbgWb%Co&BWXW+Y++=Z|7Z1+opk~^Zib_~>Y#;%MQIb$t&Ml$@y% z(*y!DvP-EZ-SS%s&O0p(&Ma}p4Kwr}M3d2dZeC_=O<6xpx#D+3^FO+ZLyQ-gnmMhQ7kGzxhNX~MbtZH zffiZ68xqln0aOky9la19nJ-9_XRFjmiA`JS&2{7YD6MjWu{9hyRd=JpDga8dH?Ty? zYS<131gFWnN7;JP-}~AON!3tozvaG582B&3F6&H)D~E$P@)*(O8{~_Qlf?fb{h*vc zZevMy*Yd9HqTTI`|9J^CWe^_(&zzK)fNr`BMFO4D!V>7qLjgc-44#MAbP4cWw3-;V zn;32IVY?0pFe-UH>fgz(R*3!|YA%g1JL3N=-2i}t6mBY3NumS#9l7>6ieEtyM4gaT znNs8SYg=zdjT;eBwP{vuG3TPJUDjpJD!?KO12dxg(Kwz9tU<|;-ZPC(2(4LCe(VGx3)qUF2+>_(DNh%V)C$n`h@q)@2SJI87NJ0ME- zvECr_V%)rh0k%Cv`CY=dgPGn;pGhjXmhE83Zuu#rgT}rF!o9YuBtmC8c?x>kLkPM% zH{~|heZH_}Nd(FVfrE-Mt`l9mX(z}ls`t`Cq^UoFl)-1cwb=_{`;*e_xq~1zu;_GU z58xa%R)m*`MR9c9;!H2mgX8OrJL+GXH1R5&!Aalq^$Ru~0q)p;Syc{E3SJD%wAdR$ z6fX+(OicwX<{vO2psn-zIg|BtGUf{_JIj@1Xh9|@g6a9-qlABXiM~BZ*#t=7;F$os z5zReBjuEA&vZ8xXmVkj2fffZF!+k|@FUsY4eVLyZVvh57kFO^o(i5`ggaPjxf%eMh zNlRZ^(H?9){bH9)_=1YXjpfLv0hv+?{b7)}oWibO0k;C@D+>y0v*(gSBC>72O&l^; zJKqLSt#7XI8H9h46U_r&FI~SFi?LrfnB!fdzmF}An0sr!aPO?vfmsYYhVc82;{~i- zzA_sQSx0Yn@&jL)6q^EfZ{YRQe}O0iSbb)HvhqN@mnTvEG*Wo&yM{ZxaP{&v-327b za`if6)tdQTssbT7;LQJIeA;g6sN4O+Znr$fz@d>~v58Eiz(we3)%o+h50kUCj#K4$ zI9E5%$Nzdg8a-IUg_rZY-?_|cQ;yL>I(Vlt^QsFHp1Cplb2j1HaP}9$Tb)ew_~>o`L6~?NK>$@w7B-m#Q@uHdDBG`4c+vAPm8?<&1j15y~Zkh((l5v z;XlXF!v+W+bUDa8h-z%_qf*t{CY^PC?`kML0+5q}rSUnWLi$h$OQ*hxI8^~S2 z97APKetTCH=!j#-ln}@V<$#2G`IUArh{mYU>16yAl+qZ*=nk;oTZYda%QEfNOVvU@Ysd?%4$}U6j;ch! zh`6S}F_SU$%heP5e6oUN{fsG^?;%t`+>+-IEx9K$;FvzbYkYu#motwdS*n8@sIl56 zzFCwq6aYoaeY?v_nEC4hQcfM02p3ELjoa?*fomB>tmLJi{7&XLue%mtUQ$ox1(bZG zMcNV$m7l2N>7oA?E7)(RxIzx(*`oQP(dl=Hh|z8=Qh}jsy7-UE^%G*j9CS%aNi;}_ z5;bzcj&is*{TILc^(#U>_cn+0-Oqui2Om@~FtSUm&QqRC$>AdPBQVO!y*|x{gKKYb z^s?^ZsX@8L5WEiOR7C%A37$*a7K~2qxkdp6h||@5VZMelb^(x#ugM(l(7=3rbjU{9(OPZYrzE>p=N?!kuH-H4lSq}6i z6AXifBu?bNT346YP0E$|oq3g6_2)izw-E&%o^$(5y6?ql7Z*kRiRdIi{cif=Wa#+# ztGj#s+9}pWRX=S;N*ZmZYOAP;NtD4kGX*MzogSGNTOO_Qoi$T9VdwLsC6?1>@pBFb8tzY# zpMUVat9ap>BARfRq1oB%@Apd2X~yUMt&Y^Sp7nn4+>lg{$Ui%G@?ycyp#pTjXzb zH^^&L3;l{kEf|Cr)z-o|jfWE}i+HW%298{RbQFEwgtP64nY^xl-5w3npnvqOwh-Zon=6&Hg!7yiBSfgA zEs=vxVGi*oXm zJ80VaQw+)p z8Z=iPoRp?O3wt1eu@G15uBELBJ4&O9Q3W>#c&RHNwF)yfUEU{nw9?PE$8w$tk98j} z6?Lc zI~sd0F_4G_)9b;yRe`;D*M)m{oQpY%SPv95)4Q9yUVB_r-s4-{Ni@`r$a)A&xH%NF zRme}#0mj}NlFsvOSK!oP!!K}BqZ}|-6O*ikrx8YV?s@t(Zjou_gM^&*bBR99BDfZJ zTT4)F@4@lU)sN`UYqzuz7HhyBhf)XyUQB;ziW2Rwi(X)VXmr)D6_*UZF)-E7LQ~-= z*Q>;C`H6DsI^UW)df4J# zzY|Y7uXg*2&iqQrgL+{B$DPx3uhq|V2B5}kyN_KSep1}4vjy(>+Zo8(xv`E~3^Sx} zH=%6D3;y~IWgv~SJJ{Fk46we|(rgI1LFI0!XNYQ?`0n28vrmO8EG1foH8^)~=uI1L z_3)cCV2c!6ZGtp79sJ5E4}mw<#MCE^OLo!~MrHgAfubXP?)%Bt;Z#dc$#WUmjNe!@ zh&ghTVW_r)^uC+TRYP6C${)F?%yj5ob)-Ayu;29up5I25M^Z0&h12eWPGX5*dt79G zp;R2*hGa}c$1w!ThFZshR`n(M3Ke!nb+tZ1+B2cl($-_%Kgz7z`WAe5vaN5ra+O5_ zaj?l#tde*Q?Bq%run-tH?c1MyR2EuvtCH+T88~A$OHcvL7QQx4hK`fY&&rzE&d?~25 zWPC6wP4xcRlk~Vm(PAT``nGqr7~Tw@z9y;_=0GZ?Q-`dSAUq)(+5X@ZtI!ys4LXvt z)kv7ZAs@ukW1JS*OVo#}0@@y`PFCY{McLYFt=AVyXWW;2vIZvZrxlbepLU!s z;HvN((e}h)v0U2C+O;q=l_xQW_xi1xH6l!p?X+TsJ}^sp#oaJT@WGEUN$bvTA(qZo zyQ#l86q;Fn7;??L30+PsXuTLLuwq1(rpdB4c6m9x7mf+KLWrJqIZ#XdbX9zNF*q}? zf2jjayF9v$|9wnOYQ2 zL;knd1_#tkYPSP27~M&BI*Un9du|*&XId0!DIVQv;(sWNj$x35U$i7qU#PCUad9==I+la#^ z`}8GEh4VVKIMeEJpX_N(vm#$)92d0nrw%W0fd&k&6|LzI&(kN73{ znCYnQSGcY{+il~HPU_Fo?uQg?Q&j2SI%n0utTvqZBP+Gaxl|2U?*G75!JoB_@PcS}E?`zn4;F}pe+`Dh@(X8KuSF~|F7Kc6plg|^YLyuN@p zbJ>T31N#Fx#@<)m;j=wy%03gmsa?V4`h$LMLXk&%s9NhV%ie5=ZlQNNW?0fn&UEdl zJ=_9k)(QFXdNt#A*~MYK@UcEm&GqV+L8Dd9y!aTW8yD&lj&}x#gkc_}n*lBy&trtu zg|?a!d_K=HtI0N;Vmk!RxE0Pl*-JmAWxr%HAvy9)j|<6&xTm^}4uF`5&|cm63fEc` zv!75qrWjRQSnK#Xr*p|VPGXs?sFi6CHT?wS`;$_PvyYG- zZ3Yb|Dfw4gz-rxl>(5A{sX6EA`6`r`Mrk`CoHK4~xbdIl$x=2a#nUiCm3ci-X2M$d zoUC7b#_e4dIm}Glt%PN3dcb$PpE_=%rxtd#OGNI%^DJRMtUVTMrh3x!SJ$>`MtcE& zyU7IC^8BY9yWX5OA2VDFUOXxczW=J-K@Xa^7h1p^BhPq^Yhjv~We>!0UXLjmUC&6v z8tLvCEypd6i82#%r#1~a>?18zLI&%r04W_0ovf4J-twapF?cdZbBNxfALvT6c1F!T zaVT2w@KsM<$;O=!S4Ey^hi^&Wr02DWJ}jW0%)D}YH`7Fi|N(=T+ufzO70 z*OYE^sO?hvnSdR&i2kx2>jgzrL@2%7@FyW0cdXb9Q>dZZ)be(#CmX!&;6Bfv&ECB* zE8^wYK~O{y#7Jb!Jca!kZX@ruem5eFG4Wm_uZf_EU@wXceQi4yX42iVo3PJI!lkz; zq8U{@U5`~;I9gr&^m6@>UwlvRqN$_&cc@>SzHVG=<*qVAd!j7uSxXh``)8(+rsp~b z5D~@Y4eiyu9?Ctb_Qq>QCAEj?<(0dnfgxPhgrx`B4JKiLnYJ4{HlW)(jRHjLIhpJd!1Y zS@&*1D{EbtGG2?pk~QvWBn?{pYTh*8qSc%_=K=1#e8A^Ny3swE$n3*4MljvrH~-=! zXLM6L>n(5S_NvlR@uHy~n^eP2y+CJiopBpE7s`h|Dl5f1;s-{JhzB8QIQa9de6C%Fd)pw^a6VPIIAPdItbER* zmUj^eX6FPvP7fVx&R%2M0pw;2e1D>DWZ^?6X>UjouY0>%f2pb7F9EzLx#R5sbo+7m z-buWk(YIaM!otzoZ=WCYUZ^kAL6V2xsu>(78q&>~{>9hWTqlswYl5agM z0dI6vi5)ywyv}``^LKrHai0RL^4&L@LmyM`fpwtDTsy~m=BJd))vk?~MEpot5;;3F z7AGSk+dr(zxKP{OZP@Ghbu@i8I4v>kYxW$BTx~7fpweICsHgPQ%BQ`Vw~O^#f00P> zy=AY+)4qSXn2=LJ8I;Mmbu-IgT`tc0mOva9uUf72OTW%vl}uR9fdLH_Q(hhC1`K^E z;Eaezi=7mco~{ABOVzBiMXafsaJ+3?08!h|Ljo~u^-ZYQ>-Nl^YQs;)n*MMk(KGhw z2mXfK*cI5gO5&IUq>Y`2a)^3sPgtBJk|h=?Ami=?Q^NclojIlPLM}7FZmEg=(+h9|YQO$$ zM{@ij7jRp~V7*5>s|xsWq*S%oYyv2_54ZlD(PKvLJ8J-$WW2uYt$N4ijQ;2O4T8$p zICvN~;TfT$CuqGgb>2IWKR?VS#FhY`Te=ST4A)GV?TMqs7_#BdjF)NLI}W=IgL)-_ z>^*MfNc&6GJ%Ri7?>WMi}NI6pCcIID9B_8s>?_ z{)WE>^HYd%00cJro-fq*S`O!}u|v)MM@6iG#blXZzDC=EGYt}LN$^p{aMJHuArpiW zy{`2mmxnb{_VK_Q6?zco%swjPf`F`XBjaQT3Ji$SZkslKwoHc?S!CPxT#JeK6D=SU zvrW(haw$Ga`INm9#4#=j!M<9*kO%xX8P21QS{e=n7RM6L&D((p1=Rd#`s5;=4%kMO z8?&Z9o{NkRNsl6HF#SSRAa^4m zq_F^>3;5|7j#>RMvW8)nMnwbbsNdX`m3}<|p8*|(%6rZF5^=2k zHeMC@iXk7@m@mGBWHFyir!p>RNrK*FQ36L;Un=ah^PA1D0`S$S9UkSHVtgSDZ=No* z;w2|Pqk%bQ8syc8=~td?HWj0`-33QC04oKOo=tpV>Ys8X;vkX(^i$HHaydKv0lp@O z=&OKD9_zbLbXrFrG=E5-qMawFWeIVuV615THv z1_!co?{Nz-pPjFp{l@BxGD?72>24v+?rX%;y_IWcs>6E^meZti@Rgi{2tLqAo#a`?9NkW~Y%^cbOGehFVN(z_4JzN^vNaP7oh{V2vt>>hcoX8o$ z(IR{GOX!jbDww_#U^jQ*>G`wfB13yTC{7}sF1A;K255#2AO5s z0e&wCrJUnK*%o+a$C*sm8VGE34Eg?wY|u|t2P+}!l9sN&DbF$Rbc0jmn7a7fqbQ-p zLznYKq1!SDHJvnjLxn{FTykceJN)&nM<~Fk@+;87s)hXc(9(8&Qm6&p%hC1_xe%9J zNjcxb(H+@5|8;NmX#N$GIcmJKiwHF0XL27v`A2?08a!i310zHTIgI??X6DQg9$sh9 zzV9B8hxt0tizBJ6@(qduY#JP|1LFw?zpGEHy=K<(^>DV$Oz+HJA zao^JfsZ}dEy7e{Dgl^ja7(1?)Q#7f2SggP+!r;Xmf@3jnDo&QY0gT%l-~kjrqEG$M zwgvzxw=DH%N-pe^Y>JEg{7aA9Yt4s3|BL8DW#O+BVA0X02Q1yo8S4vX45L~P6S-^_ z-C@N4U=c@Z|kJMZNdc@T8-;*Zf-bI)RSkv^@Y=5V=(t-h`z+0dZMmlah?&WxIn|GXIr!4waNGewbjn=uJtE8xUATAFny5QoZ2-VB0`sco2u~< z+wY#add+_R*Ms$z=Rip9!@;4q&EG2GHpjC!wI35&=dHn*m&=5?v6I!j3hYi-Cml`- z&GP*sCdu;^yVf^r%71Ws-Jw+cT3iQbI&W7pWL$qCmML!Pva}J}?pdn2;f{owF z=J>p?C7e?Ctu;l7Qjz0L-f$Q@BDlss&;p05uX$(u(G)IqF@cyA%_T?W(-0=+CusSmx|JrF2U&_J372dIZn$GOEnY`B7C(XO|14!=89^RiOZ>{fPU%;IN9hy}2R-ArJJ<>>* zBOY#~0BGb~a+MAcsoQ(({OfvR(s%1&W=3FVBKPX?R-5Gd59?m5H&kiSzgMaHDZ7YW z9&>Q59(9b0sQlm!L3YQgc-TziSh1KV=y!ou0Q;%%)INvzz%DD3`Rupo*cjpw{I9cZC2?@l#fNs4E3}7o*f527p9--;s8H z332_^=kyiq&>TXc4qWP^aU-I0@;_ z_dyE$Te0za!(9jX4LiWpZqOR_@d|tCjH6DAPU(iDhu)m^*2BW%?s2`g%auT(&7~23 z5$&_X2{>AyEHLbUYHP(l3j&KhS*rVXw$pj+(i#m1K?xv-@~-4_uNsIHAUZX+&IO~} z>gFF_V`^JDIC*tKYqpp0Sp-%C_uD(|v#wzV=tQetUID6IZ!dA_k9ov_IN75!3+Gy+ zStjoK@8xhvPM_~r#IgiQPkA)Mo~a~LLpuVs+i8)|%pth30#jR`4A4UqIx#0=e0&R#u*f_&;8`*#lYYV?T1;0*9pOFK( zAbwZho_@pwmU|Z-&&}xloez z-%=h9dFD5ez{gV*p9K593%(^g4f6=_c)5AJR;>L~z;@0s5Rd8TzR7gG@8V7?M}=7l z+iIDH$xNBZk9BeFG~X81@xVkARFX+?s-*UYW&@up9;xh#EQdy4=Le*-;Fxd_<}`EL z_-s|LD*!^&IXhTKSfQK}5`Fsh^*&F^CS=@NUd9(t@I7|?{;GZ=dG!zqorYcSyb0?AN}qovlA23eXPZI1_5++6v1RVcZ_rEOHc~?5mlxaTPc$@ z?kV_kE?LteN|_`G)r??28CC;G=K-bKUy)1p9=Fu`aFWrL7-78lm}XJ{+w;F-_k$Tm zSgfcVZ9+*o9}QVV`v9zvbjY0nF1(`5pj;~tj>+aW+$5~t<6&skqN-aKDX>GsrqFO%@d@CVhqz%Bb{~Mz!$#g+i`I}} z8PMIBD!E&QySL$)VCC0f^-EG1J55Fifv_dIK>DJmRYuo^e_z!zcnK&C)Ztq=Xhefe z-!}3?lx-|-OBrRK4CKMg#+&BoR)RzXS3&7(9zG`QT#CxYL?UZ#qSc?Dqk~>en<~pL z{55hMKU_$J7cH+{V)^N$aG=Lx#@FijIM&C&TR$|;xj9C#g0&lz@4ZzwT#Flm@9z-y z&417^=y>nJ1T>*$Jn)6M_umxo&2;8gR{XNVCW1Jc&IFUi_%iKe-~C|5S}3>3kC}xe zzBG8|9;=$Y>hHj(m zr@j|)y`A+p5~;9O@kUx+EuLZ2yp=wU$fZDdxM1z689M}uYyUF#>lGpAI(w(V{>{vH zzpaAcRsTSPN`w)T3ot8WT$~1=7gRlG8i(>*wf*(k5#bP0I0LXlb?~RDCO{vY31}DD z%2F}uD2v7|<<4b{b7`wmUoh{S?qYb2IA=n|PBjjFf;PHEFuW;coWQMbI(JVAmk0HX zZ4b5`4?>Kc@@bK~vnmd$llz%9gvthGYSC6;opoXb6&uG{|8v;>w`isb9quTJ`QRB| zLfA5a`RQG$3T57)pw2VF&}1MkV6L^~_mx?W=St^0(bA_TBa>2b&`F!33@YT)dP2}h zn`?;9jILyXW_bqx}%49sy_lMwIN5PMUoj26 zjjS(=B;I_fEiY$wCC2w_p3c}_`h~lo^0An7I^=6(*y#`PRs#|MB=)vKk>^hfpvs;f z33`+$lhHk-MVYUJ8{cJGu=mX%Fc`5C_NTw8F^P~H^!$pCzVU10O{$0f`avrG6fiC0 z&6MBxz#|^JsNnQ-H7DYj`OLwRP(U+xxZgNHbykVoJ*^B@8A!z3)qG^zdGdJ zT|bww>9buEdy^q{AY>k2%v&7EK0~Xm@UlVuny+*7z7oXX#VyFfq<*x6THbFmy!PEd zCV1G|9i#V7f=1v24&kdG6(2G_H&kaaz`K5rdF_k?SYp`6{P4X_9TA=k$bJ;;GRe+- z8>ejM_^4TEK>eLqR&~E7LuL626XF0g(vMVS1>Tidc(E(rm_od=mIyR>pNM|gF&yKJ zpjVX){D`;e2c4E3&pOnY0*h^_Q$8x;DPJZ+Ky!5S{7mgV{P))VoTon@h8zy~uIl;g zXxR-IQfV)w$t`#w<+SJ3Y}JjjdC%8BA1-=Z_GCP2&?xoWTQAd+Uucw}0Km@=9mJX- zM3g*^AK&`CZcBtrr|d1fWla-ty<>Euh&ICTz*CWi>rby|HYViz3iG0w500NDGn8+HzBAF^1bowwRNfXgnYD)2@jPD61(#ful< zGn&G-Bld1TIJFT4Wl3f;s|)Y>cs74qdduBbg;PZgIJp`mM$h(2@hLX$u|{Kb8L;>W zNt9d}bdNsiBRN2ObE!jf8lfRdJ~+Gr)6as@g{G%NKm#8EI)s%+@T_Io1NQcI5ThFe zxg)iVw(}2bG?4PGC7-hq0>62yA%h}VJ)qQeeLQuS*iz20m0MfHO6fY;+0b~l7-fhd zkUp+~HxvUu{pp5#`0*O@hLE=E{k+gwv$5XN@YWhYIpFKwcWxj8r84R73RFI}e{;hq zbK?3@00UE*qr;Jz6%$y%P_K2);{C6wdJk_t+Jk#_jC0IS(>qR4N<4Vq^va$D&uyS@ zC49ds)1QnEp3=QNzH7Q61ZO65;>R6Uc61Mti@Ya5R? z*j5!*tSx}MGUXhkyXES5VUr;Na%{D$9=W4wmE_CqBWpw&muu%ejflPECJ8bX5+hD# zP_gLmjHjXu;P`6L8#hxHT?pmM_BIby=D?90)e4_B5M`@NBe4rD{bF+2bhK)rH5nN&M!bpIs0XvC*V%X>K%JO!rGpvSOA~rEx&YO@NMOI z%|n8gYAm4ey^{Z+l;Eb=2T~F==}-H|TLHM5hOP%dw=ggc(C%j?7nJ z>^%6dhpdwLYnbps3-muda5Tu*PsC%sIOB5%q(7tC4Gt75?+xRVIl!7kXWS&Y!DZ0B zsi=H9|3shj@-41G@{g;jOLhRYv1<%REc>xvs$2)RqhG-Xw1Dk;M^l#df?7rUUdq1^ z#;KHjpcN8(64WGUu@GPO4pN}QBk)r7{f%N3HxP#*B^k-y+Eg=!ZzZQ6ZyN9Q>YvuL zt>$B>DF)rau3lKrY~r}HbF^2=VQ$9R1dRgbV?;T|Awl^w=sDdbtmk zeEbmbByD$s1W;G!2S33+GC*3D{FU(7SU*~!xx94|ESeCjpi9TN>XaqZs7FU_c$1eM zkqpB-^Zz+|>rjCBaJdjO;ti*#j+&vf4GJ61eHkLQSvAnVl2sXJ_MDL3Rs`-ct{WYM zKN^zH=A2KL6x1Yp}m$JPlicUwDZ=m5!RfY@YMf3N%Wy- zs*Ua^l=Mb}hGiw_*$xr=V7LHQOcPue_aGzhvJvI=RBeE;gwg*Hwq}wIZ5U|*z)^u$iKAU&w|sRx9PKL5*P5b%YgGRl8h?uh2}$xJ-XWO&Y&S1DgH9u*#FTFcR&7Lj?853X^?igV3>@r^Z&rPZ!Qa(|EQz$R6 zeb(kw<7jEgdso{2rMfw5sV0!pg9cDv!AtTT1GInVVv^%2LU zEOicRJZ;sZaS}9Z@fF+q7da`x?@mw8m+QMME=BJjNrfW6xkHKhMYO$d%`QYBo|oG& z3v(w$1X;u_eKrAcpV1{PJLx-qW(c0c9O&r9-|JGr>^Y6QwPv5pcIettED)N&RHR?* z?j7P4N{LqUgV8QD-a61_-Wm7hR;~E)qqwam@nnq&B`z!6veW1+w=32R88-oMQ z*6*wql%(G&^k0NTE#aFLM1_^{c>N?Jn?TCK9f${9DQepD)cT@|q zE65lRvmqAR9{j!a$Twy>>v7h}w{7d&khje}UZ1~?QFQfZq8{n}FncKnTN-XNN`q9(y7#Xvk)UM>5RX(bPUk7$e>r}6m(Cg8{#X&^+pk{4yb#k) z_v>Yy78mk-DXXcSE|sU9Atk~-ZBpX*XNIY`ZR=`Rf9+)}J&2L;VO5nCDROS(^51`M z*FwZ;IpUVLQ@R41rx`a!5Ih*GU2mz?;4!hTKVcHpu$<7S@IzVt{3C8v9hW0J=6u>B zb9S%Hv?Iz?p^7e^3-2#Ccw${-o;6hb>B*QWN3469%W&B0<}NXL0v~|wa@n)__mkZQ z24G^>^dFxx?BplFpAMQ?koW%#uUa(=swccW@^oyRPZ#+>Y&YBZYxj0VDlEEAJ-`I~ z;4=#-j-3;vLjvq>GF_THPgQN1O z$}uv#f$yq&av}CxAnzrw>#W6VY+uB&JDC>}3fO~h{+SpHiIDBDfgDo;I%gx!t2yQ8 z6}#=;yxq}v@*-YN+M1B-Z>-&_UT_drIxpZ2>_%;G zw4z1)Q&%!yN|J@Lc{TJeTkm{iIyjR|RrUaM`)bipT3h3f%V}aUdtSKW8>I8R6HZiVeYw~N5y3 z2S)jvIU17XiP>7WtxBol=Y6Ut%CURTnM5G&|APq>38wP!1j{mj=Li+X!szT<3Esp} zeX7CcjiT(t>DZ*x_rYKRX}s#yfABg&2jKNcox z`XqUZP1FrQa_6W*I;$+h|AdDs!)!glLty~xxYTviF>tcf!KLy4o1NVF_4`#Enloo9&s6pHlRGY*R^-YhyL%v%c;XlYxxhvORsS5 z)zsXp@zhK4|9kr{u7HBea?I~WS<9OL0c*nsjJxGw&U?mpSAc(Il33X;0L%0r2Q}uA znU^~n@5YMy!J?5?y9@l6F5o{@@c=7-kM{uNOiy#&i6Z_Um$a|1Rh;(vcmFUvf-9he z0Z;%(^TCa2wRr;v;p%*RCGp;=K5Tw?@9uuU7&mW`(z)8vHRm|V z`lC+n|I}WULxFq%RsI(t$wV0Aj3~8NosJP--CghM2O+HJ!UlygKZDB<9dWHkT`b{Q z-x60ePa69x@wg~dDR$3;vDJVUIVtT2WgIT40m^3_B2h4^mX;BX^!XBe-Zm@Hde&;? zcx-uXk8f&=ibjM2%}mL~l71CC=N8gTHzHU9I4w*z%C8kKmtQXv>+$W!PV5CR;hHz* zdiD}s4O2T5-mn^KU`?awKE@vPI*a)`f@6Nh0vq*|F2qRgm^%(vxP=HO6o&~7TXPCq ze{=*g&_grr#Gw=vQsvz6FmSNTxRv%p2&Lmd{GX6L_c#qrYg>7SuC+YcJgr<>@q57t zzgsP4YOu>T+vK_3NE9m#FhxgjEd}iMTxCO3Jis51a-vN@Zls$Ri}q6RtT43v(Xi|7 z6+0&MUrAF#oue-vpk!|4ddN!zP@(SVCCmyvt`PDqn#;LGK?t^8X=s>e@5}Gz+YD-G zdx>`hPuwr|W-3D_p_#b%rf}_mTLdh&>#B~!+t$rRYp?sx%>HQfGcy`((S2-!9^nqk z6tg#L5kx|~fyxok$XKh4u#(wVxaAwfxnO!;PR=%LVqt(E^88Q1eG-b81*x;^VQ-kd zlGkN%WQ$(qNq_n!N@}}Wi%;>1x;;xs`Mb#$6%&urTrPz2@}(+Zpf0#nsO^j8JCbMwoi|6 zG+GT#h;fY0DE!sI|5*;4DRF1MNQs5{vBt5Omm`$ADf|{!=JV)V0r~?(aE%784;}$x zRIez-8tFkwUgJ+NZ};@(Wjyz9sE*OtORd7aV2k3B?1+P{^OZ(N3i(-3ciy|}mSZ)W z_&Po>=uh84Rh=}dPGfs9C=pn%-{D4AhFmI&KWzs8kq!I|Abw=-qYstU(9)a#U3BS) z&om{wwmJLM_NKV9S#kgzUX6SBS?U)>5LXZ6fVa`fK!7=ZT-By3Yk{D5o#2s6Jqg;a zNV6AbQnhz%crugmVSLN4X<|2vsMEyzqDP_fBFEX0_A=Y&oVDC90ev z&T`~Aq(tmjk~vcS-yMlG;NXi5XN&f^QA#xqK_Es8Pa(WEb;!M1TNIuM6ih1*e|Osz zKA?*B84JV^q;H)DKn^;~eJ_LOE3Z zo=ula6Zg)HZ5p^3{FM9-lqAn2!xw{XYr354s6L$7HB8r2?~W?^BODaIolx!$6j3E( z4Bb;amlJ9<4Ml9z1Pofq^kX{Ot$z~x@Zz50maW&6#04^n*T8qYJ+a|&j`$ko{|l%K zSM-klOKutW9CdzTmhCoeR-0LQu1uc$E=B+lf4)3lS~I$jwQ62(mVY=jY2Ebb*GoS! z!=L_zSt|7i!6PTGk2dWO#=mtk9&-`B_v><6mcNW|P0Oc+$5UQbRzHih6E5?C`%VPm z_gBC39`oEa`^}>_9X5Zm<3{tn_iYH4)mx*8fA_*m3STeNosX=!Dwo#*0Lp;E3q0M) zEz(YrGSY7eojRZY3o}UICS|fhr+&)tWaVYaWS;0w%)i1AY!-m(hO7y_0o;E0osn^S z=_h9J)1R4vg;S(6OK+vmWgfG`ke-Ip(NK4 zdAIrJSL`+a>i6D1mgj;l>-_%P?i=Yz$|e<^!__Um{U81;xucXj z!qG(?E6ZCyv1M@QIlAN3igePf{8T;(G>i>^Y7?J0N{oU^Px_)4SId3D^kB$rIVU}J z!RtI7hg_V?*ni@9tvXK(SybQ7OX)jzJ&xIoYXBDU1XmS+wdqZ~w$M$bBi18)OQ+$w zP*B#onA$yYbuK!Oc5fmb?Y|U-(aw%shiLk3&OZffmTqKZa)-%!JxeazPA6P3)$b7sQsi) z2k+Lxx}oQ}T3iFLNIT5}aC8$E*3*!#==5y88I4qx&C;zE$W5d^a5rC9)q-o@~-H&5<*>hmITj8B(Kk2l%kgr3HpRstVv ziKccwRR2;kUHQ@Z;HPq}N}K+2BTrG}qnuB-MUHPwS+d>ox@;((*Osq;Az7jHtbV;< zdKGNa|9JW3QULt7zw>+MmBp*g7p}g=oVfZH^V~JJnrE&(U_N*4@7tGPX62V+Iu1a& zFhWWhQZSvKp=@J!v(jExeccxwckAE|5r)7`*XKpz8U0Urd0Lk47vloJqVPkxRRf^) z#Olx+M-KqLpZ$vFYaO`>FIEZzP7ako2qqs>t^dCp+(y5vGqW$c9)>WlQrRcL)DFCdJ|2HVkXI_;(EIC(+Cp!>#n zk&8u_Sv3GAI|tL&8&?M)|7T`kw?8HCJEo7cWcxl!AKQ*kZ7Iq%gR&zXPSsKA(HxJ; z={mg_{^QrY^e^4UxU4e$4fP?bts>W6NG{Urw@Lf%ea^goN&a>y{%=t0`>e)03_yX1 zY&rOz&t)mw=wbIT*&1~(phzk01NtAPUo^o5JbGc7)o zs*aPF7%MGH_lxl=7k9G&Y)EgCo25u6(l>@tn$>%`DNM?%>qP5yCfFiaM9;ij-PGqg z9@1^C!%q3JKA6jPE#&rjplf^=b||P&h|Xc zi@xNKb9HUfoku=xKC<+XJ@WfHJ32`KCU>#3v#9f=z^gS@0+83sbaiia0XW^UbVoPl z6oB1Xpm|lywvU?t`7U#PT9UuS+KmxFW z4iJDe$aNTG%4}Az+6Ta1&-8e34qC1QwE(c?W9D^9+caxBKmgL{5VjR`fB+;6W(@}O zI35D9g7KS2Md6s!2EQ%<9y)Z$@ZcO!2b$3V0+0Zt2UtM|>H;8dG}1R3oDa@NojO1O zRxo~bE(m3J=l}so!5W;x^CqT}Hrf*VFUbZuJT*`*hMzVEZB3Mcu>JCi; zH;f@^t7lLzg0t>xR7XRZ7lW)tb0GoP++0(?IoEaRK!*Vc5C}-1Ao^U#{dt3dz$Ktu z7DnO)N;|Pr2z^>$tx!qi37CEaK9B6h<)AWdePtS6I7ONB~mz>N3id+njFA zDFBCNFc_q^Hd9KM@+QZD6M1ps6-JAdPSbUC9A)*nj_2ixQn+;_ondMXYrY9+9Du1C zGPUzD(KxH<66)w1Gr2jeBCDv*`eggkeDbo&W##!~F|EmKtckWd48UlMFxd)BK`Se7 zr1W}gGBw$dC0?F2JaUoC1)#SieQBdM#6wK=Gx1z?ImprHk!P9IOAMVZBW*KGtzR-% z_03D80JJ)ozTukAc&q4=Qzz-0lRnio@X7Y2`PMtDTvnb>7SpDqckP+S4gpZzbqbu+ z%O%4EDl)lHZmFXIrj_2eeTn+2ZojX#T6Ma9DJ5Fax7(sPWmJZ51IIs(AiQ8v3(%C|Q= zZ$KIYAZ~(!`Vtv^y-VPKaY|o5s3YE_3y08)S8n~$P1{JH$7MwZQnwe$Pj**gh!pFy z+|255)Q{9|4!qK2&eHuFE2m)T?+5DZ8w5QUjCUP^1YiSqa?$faxo}4pZP%4&p8MY( z0BU`TjP+1<8;0sdFS}Lg>R=iUshy8l#`wPoxoresTBtn$#FeM|oLrl3Kc$|G-ObnM zp}0?m%uf{ZrL)-wJOP+oWV656^9-zMkG(MfrnVMc9o5F&HR!5r&?72XKXr|`b)+4Y zUbwxCKIgWn+8djrP@YbA+jjFj<4W~O@)=J1+Ga&w6Gd&nZwpb&a@fj{21RTT-w+pL}1M$du66W;$(b zYty?0>a>-b3nwobbGjW_e}=L)-3|%BMA5swkh~z9UB4y)h_y{`qX)Jfq07M+r{e9s z)Js_LGdcZ?%UfU1Z3qZD9dUnqsVz@|a-}wPd+48PQ$5#<(gpa={5W$Fo;y{a_F|@P zm?s7x1n}S-%%cu$+_=$9^WaU41})>%hx&D3A^_fW(@n-bcJJP8cJAD1Hf`Eu-v9ph zn+q(;Fnf*Uq$Fg!R1v!(;M za0oyeQ*BHc@18zP9e{iH?ltfNp$zZ@ue|a~vt!2&9-M=j(g6and&Z7{;+nZSFm(Xl zc;k%*5Ztq8j|CxM$b)k*YdSyx(wMr&l;_>kiKzono(TekJU9olssp%icyJD=1Krnw Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| diff --git a/website/docs/assets/maya-workfile-outliner.png b/website/docs/assets/maya-workfile-outliner.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd1bbd03bce52041ad511dac138479494641361 GIT binary patch literal 4835 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 X`o37wi;`y6#S|Ob%*wRV#3TNnn_AI~ literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png index 7ef87861fe97322a3b23c28ee3644b76ce995e9d..1bea5b01f5f3145e2d1aa5d46545c3739aaf2584 100644 GIT binary patch literal 12596 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 literal 29814 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 From 90754fb0b8d36eb0b34cba6da48f672505470f0e Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:51:02 +0200 Subject: [PATCH 188/194] modify readme doc --- website/docs/admin_hosts_maya.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 0ba030c26f..0e77f29fc2 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -160,7 +160,7 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![build template](assets/settings/template_build_workfile.png) +![setting build template](assets/settings/template_build_workfile.png) **3. Build your workfile** @@ -168,6 +168,6 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile -![Dirmap settings](assets/maya-build_workfile_from_template.png) +![maya build template](assets/maya-build_workfile_from_template.png) From d3c2dc57d8f39865c3f3900db1be1f911b6436c9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:02:43 +0200 Subject: [PATCH 189/194] fix linter --- openpype/hosts/maya/api/lib_template_builder.py | 6 +++++- openpype/lib/build_template_exceptions.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bed4291e3d..e5254b7f87 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,7 +43,11 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') + placeholder_full_name = cmds.ls( + selection[0], + long=True)[0] + '|' + placeholder.replace('|', + '' + ) if selection: cmds.parent(placeholder, selection[0]) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py index d781eff204..7a5075e3dc 100644 --- a/openpype/lib/build_template_exceptions.py +++ b/openpype/lib/build_template_exceptions.py @@ -32,4 +32,4 @@ class TemplateAlreadyImported(Exception): class TemplateLoadingFailed(Exception): """Error raised whend Template loader was unable to load the template""" - pass \ No newline at end of file + pass From bc9c5b183171b9ef03b88062c7536a5effed3ae1 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:22:33 +0200 Subject: [PATCH 190/194] fix linter lengh line --- openpype/hosts/maya/api/lib_template_builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index e5254b7f87..a30b3868b0 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,11 +43,8 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls( - selection[0], - long=True)[0] + '|' + placeholder.replace('|', - '' - ) + placeholder_full_name = cmds.ls(selection[0], long=True)[ + 0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) From ba1abf8b15e1476b430180bc63b1a3801ff118ef Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 10:35:41 +0200 Subject: [PATCH 191/194] add task name field in build templated workfile settings --- openpype/settings/defaults/project_settings/maya.json | 1 + .../schemas/schema_templated_workfile_build.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2e0e30b74b..453706ff88 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -722,6 +722,7 @@ "profiles": [ { "task_types": [], + "tasks": [], "path": "/path/to/your/template" } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index 01e74f64b0..a591facf98 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -16,6 +16,12 @@ "label": "Task types", "type": "task-types-enum" }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, { "key": "path", "label": "Path to template", From ef7627199eb8297196c9d0a778e222132d742be2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 11:37:08 +0200 Subject: [PATCH 192/194] add a task name verification for template loader --- openpype/lib/abstract_template_loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 159d5c8f6c..e296e3207f 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -160,6 +160,8 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue + if prf['tasks'] and task_name not in prf['tasks']: + continue path = prf['path'] break else: # IF no template were found (no break happened) From 01f2c59049be47fce42a5bfdcf67ef1227be1d11 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 15:15:40 +0200 Subject: [PATCH 193/194] the update placeholder keep placeholder info when canceled or closed --- openpype/hosts/maya/api/lib_template_builder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index a30b3868b0..855c72e361 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -107,11 +107,13 @@ def update_placeholder(): placeholder = placeholder[0] args = placeholder_window(get_placeholder_attributes(placeholder)) - # delete placeholder attributes - delete_placeholder_attributes(placeholder) + if not args: return # operation canceled + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + options = create_options(args) imprint(placeholder, options) From e6cad709cd5904087f687a4e2ec5a15641ab48e0 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 18:43:34 +0200 Subject: [PATCH 194/194] fix error when updating workfile from template with empty scene --- openpype/hosts/maya/api/template_loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 0e346ca411..c7946b6ad3 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -80,7 +80,11 @@ class MayaTemplateLoader(AbstractTemplateLoader): return [attribute.rpartition('.')[0] for attribute in attributes] def get_loaded_containers_by_id(self): - containers = cmds.sets('AVALON_CONTAINERS', q=True) + try: + containers = cmds.sets("AVALON_CONTAINERS", q=True) + except ValueError: + return None + return [ cmds.getAttr(container + '.representation') for container in containers]

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 z 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