From 537b9e7bab559419c37460c1c67d709bb482169a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Sep 2021 08:31:18 +0100 Subject: [PATCH 01/62] Stop timer was within validator order range. --- openpype/plugins/publish/stop_timer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/stop_timer.py b/openpype/plugins/publish/stop_timer.py index 81afd16378..5c939b5f1b 100644 --- a/openpype/plugins/publish/stop_timer.py +++ b/openpype/plugins/publish/stop_timer.py @@ -8,7 +8,7 @@ from openpype.api import get_system_settings class StopTimer(pyblish.api.ContextPlugin): label = "Stop Timer" - order = pyblish.api.ExtractorOrder - 0.5 + order = pyblish.api.ExtractorOrder - 0.49 hosts = ["*"] def process(self, context): From b6754d8827a1761d2a70792713bf4317a5c6a25d Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:15 +0200 Subject: [PATCH 02/62] added single selection option to task type enum --- openpype/settings/entities/enum_entity.py | 57 +++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..6c0e63fa1f 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -376,11 +376,16 @@ class TaskTypeEnumEntity(BaseEnumEntity): schema_types = ["task-types-enum"] def _item_initalization(self): - self.multiselection = True - self.value_on_not_set = [] + self.multiselection = self.schema_data.get("multiselection", True) + if self.multiselection: + self.valid_value_types = (list, ) + self.value_on_not_set = [] + else: + self.valid_value_types = (STRING_TYPE, ) + self.value_on_not_set = "" + self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (list, ) self.placeholder = None def _get_enum_values(self): @@ -396,15 +401,51 @@ class TaskTypeEnumEntity(BaseEnumEntity): return enum_items, valid_keys + def _convert_value_for_current_state(self, source_value): + if self.multiselection: + output = [] + for key in source_value: + if key in self.valid_keys: + output.append(key) + return output + + if source_value not in self.valid_keys: + # Take first item from enum items + for item in self.enum_items: + for key in item.keys(): + source_value = key + break + return source_value + def set_override_state(self, *args, **kwargs): super(TaskTypeEnumEntity, self).set_override_state(*args, **kwargs) self.enum_items, self.valid_keys = self._get_enum_values() - new_value = [] - for key in self._current_value: - if key in self.valid_keys: - new_value.append(key) - self._current_value = new_value + + if self.multiselection: + new_value = [] + for key in self._current_value: + if key in self.valid_keys: + new_value.append(key) + + if self._current_value != new_value: + self.set(new_value) + else: + if not self.enum_items: + self.valid_keys.add("") + self.enum_items.append({"": "< Empty >"}) + + for item in self.enum_items: + for key in item.keys(): + value_on_not_set = key + break + + self.value_on_not_set = value_on_not_set + if ( + self._current_value is NOT_SET + or self._current_value not in self.valid_keys + ): + self.set(value_on_not_set) class ProvidersEnum(BaseEnumEntity): From 64e7dbb3475e326d999b094c88d2c9579b36eb4b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 3 Sep 2021 14:46:29 +0200 Subject: [PATCH 03/62] fixed valid_value_types for providers --- openpype/settings/entities/enum_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index 6c0e63fa1f..ee54bc6e02 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -456,7 +456,7 @@ class ProvidersEnum(BaseEnumEntity): self.value_on_not_set = "" self.enum_items = [] self.valid_keys = set() - self.valid_value_types = (str, ) + self.valid_value_types = (STRING_TYPE, ) self.placeholder = None def _get_enum_values(self): From 8399de95cb9cb71e3d8b185267724aebb7256ce4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 3 Sep 2021 16:51:11 +0200 Subject: [PATCH 04/62] nuke, resolve, hiero: precollector order lest then 0.5 --- openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/hiero/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 2 +- openpype/hosts/nuke/plugins/publish/precollect_writes.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_instances.py | 2 +- openpype/hosts/resolve/plugins/publish/precollect_workfile.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py index b0b171fb61..80c6abbaef 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_clip_effects.py @@ -5,7 +5,7 @@ import pyblish.api class PreCollectClipEffects(pyblish.api.InstancePlugin): """Collect soft effects instances.""" - order = pyblish.api.CollectorOrder - 0.579 + order = pyblish.api.CollectorOrder - 0.479 label = "Precollect Clip Effects Instances" families = ["clip"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_instances.py b/openpype/hosts/hiero/plugins/publish/precollect_instances.py index 9b529edf88..936ea2be58 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_instances.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_instances.py @@ -13,7 +13,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["hiero"] diff --git a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py index 530a433423..ff5d516065 100644 --- a/openpype/hosts/hiero/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/hiero/plugins/publish/precollect_workfile.py @@ -12,7 +12,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Inject the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): diff --git a/openpype/hosts/nuke/plugins/publish/precollect_instances.py b/openpype/hosts/nuke/plugins/publish/precollect_instances.py index c2c25d0627..75d0b4f9a9 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_instances.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from avalon.nuke import lib as anlib class PreCollectNukeInstances(pyblish.api.ContextPlugin): """Collect all nodes with Avalon knob.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Pre-collect Instances" hosts = ["nuke", "nukeassist"] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..8b1ccb8cef 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -9,7 +9,7 @@ reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" - order = pyblish.api.CollectorOrder - 0.60 + order = pyblish.api.CollectorOrder - 0.50 label = "Pre-collect Workfile" hosts = ['nuke'] diff --git a/openpype/hosts/nuke/plugins/publish/precollect_writes.py b/openpype/hosts/nuke/plugins/publish/precollect_writes.py index 0b5fbc0479..47189c31fc 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_writes.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_writes.py @@ -11,7 +11,7 @@ from avalon import io, api class CollectNukeWrites(pyblish.api.InstancePlugin): """Collect all write nodes.""" - order = pyblish.api.CollectorOrder - 0.58 + order = pyblish.api.CollectorOrder - 0.48 label = "Pre-collect Writes" hosts = ["nuke", "nukeassist"] families = ["write"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index 95b891d95a..8f1a13a4e5 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -8,7 +8,7 @@ from pprint import pformat class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" - order = pyblish.api.CollectorOrder - 0.59 + order = pyblish.api.CollectorOrder - 0.49 label = "Precollect Instances" hosts = ["resolve"] diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index ee05fb6f13..1333516177 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -13,7 +13,7 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): """Precollect the current working file into context""" label = "Precollect Workfile" - order = pyblish.api.CollectorOrder - 0.6 + order = pyblish.api.CollectorOrder - 0.5 def process(self, context): From 478693d0ab6dbec4b76c8d9b80c6812766766895 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:47:23 +0200 Subject: [PATCH 05/62] added tine addon --- openpype/modules/example_addons/tiny_addon.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 openpype/modules/example_addons/tiny_addon.py diff --git a/openpype/modules/example_addons/tiny_addon.py b/openpype/modules/example_addons/tiny_addon.py new file mode 100644 index 0000000000..62962954f5 --- /dev/null +++ b/openpype/modules/example_addons/tiny_addon.py @@ -0,0 +1,9 @@ +from openpype.modules import OpenPypeAddOn + + +class TinyAddon(OpenPypeAddOn): + """This is tiniest possible addon. + + This addon won't do much but will exist in OpenPype modules environment. + """ + name = "tiniest_addon_ever" From 3dd69032510e8087a889c3bf38fbde96ce1204d5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 11:53:59 +0200 Subject: [PATCH 06/62] added base of example addon --- .../example_addons/example_addon/__init__.py | 13 ++ .../example_addons/example_addon/addon.py | 124 ++++++++++++++++++ .../example_addon/interfaces.py | 28 ++++ .../plugins/publish/example_plugin.py | 10 ++ .../settings/defaults/project_settings.json | 1 + .../project_dynamic_schemas.json | 6 + .../system_dynamic_schemas.json | 6 + .../schemas/project_schemas/main.json | 29 ++++ .../schemas/project_schemas/the_template.json | 30 +++++ .../settings/schemas/system_schemas/main.json | 14 ++ .../example_addons/example_addon/widgets.py | 30 +++++ 11 files changed, 291 insertions(+) create mode 100644 openpype/modules/example_addons/example_addon/__init__.py create mode 100644 openpype/modules/example_addons/example_addon/addon.py create mode 100644 openpype/modules/example_addons/example_addon/interfaces.py create mode 100644 openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json create mode 100644 openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json create mode 100644 openpype/modules/example_addons/example_addon/widgets.py diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py new file mode 100644 index 0000000000..df4d61650b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -0,0 +1,13 @@ +""" 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 .addon import ( + AddonSettingsDef, +) + +__all__ = ( + "AddonSettingsDef", +) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py new file mode 100644 index 0000000000..64504be756 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -0,0 +1,124 @@ +"""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 + +from openpype.modules import ( + JsonFilesSettingsDef, + OpenPypeAddOn +) +# Import interface defined by this addon to be able find other addons using it +from openpype_interfaces import ( + IExampleInterface, + IPluginPaths, + ITrayAction +) + + +# Settings definiton of this addon using `JsonFilesSettingsDef` +# - JsonFilesSettingsDef is prepared settings definiton using json files +# to define settings and store defaul values +class AddonSettingsDef(JsonFilesSettingsDef): + # This will add prefix 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 = "addon_with_settings" + + 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 ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): + """This Addon has defined it's settings and interface. + + This example has system settings with enabled option. And use + few other interfaces: + - `IPluginPaths` to define custom plugin paths + - `ITrayAction` to be shown in tray tool + """ + label = "Example Addon" + name = "example_addon" + + 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 connect_with_modules(self, enabled_modules): + """Method where you should find connected modules. + + It is triggered by OpenPype modules manager at the best possible time. + Some addons and modules may required to connect with other modules + before their main logic is executed so changes would require to restart + whole process. + """ + self._connected_modules = [] + for module in enabled_modules: + if isinstance(module, IExampleInterface): + self._connected_modules.append(module) + + 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 not way how to prevent addon to do invalid operations if he's + not handling them. + """ + # Make sure dialog is created + self._create_dialog() + # Change value of dialog by current state + self._dialog.set_connected_modules(self.get_connected_modules()) + # 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")] + } diff --git a/openpype/modules/example_addons/example_addon/interfaces.py b/openpype/modules/example_addons/example_addon/interfaces.py new file mode 100644 index 0000000000..371536efc7 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/interfaces.py @@ -0,0 +1,28 @@ +""" Using interfaces is one way of connecting multiple OpenPype Addons/Modules. + +Interfaces must be in `interfaces.py` file (or folder). Interfaces should not +import module logic or other module in global namespace. That is because +all of them must be imported before all OpenPype AddOns and Modules. + +Ideally they should just define abstract and helper methods. If interface +require any logic or connection it should be defined in module. + +Keep in mind that attributes and methods will be added to other addon +attributes and methods so they should be unique and ideally contain +addon name in it's name. +""" + +from abc import abstractmethod +from openpype.modules import OpenPypeInterface + + +class IExampleInterface(OpenPypeInterface): + """Example interface of addon.""" + _example_module = None + + def get_example_module(self): + return self._example_module + + @abstractmethod + def example_method_of_example_interface(self): + pass diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py new file mode 100644 index 0000000000..8e7fb410bd --- /dev/null +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -0,0 +1,10 @@ +import os +import pyblish.api + + +class CollectExampleAddon(pyblish.api.ContextPlugin): + order = pyblish.api.CollectorOrder + 0.4 + label = "Collect Example Addon" + + def process(self, context): + self.log.info("I'm in example addon's plugin!") diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -0,0 +1 @@ +{} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json new file mode 100644 index 0000000000..f6b7d5d146 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "project_settings/global": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json new file mode 100644 index 0000000000..6895fb8f6d --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -0,0 +1,6 @@ +{ + "system_settings/modules": { + "type": "schema", + "name": "addon_with_settings/main" + } +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json new file mode 100644 index 0000000000..80e53ace7f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "key": "exmaple_addon", + "collapsible": true, + "children": [ + { + "type": "number", + "key": "number", + "label": "This is your lucky number:", + "minimum": 7, + "maximum": 7, + "decimals": 0 + }, + { + "type": "template", + "name": "example_addon/the_template", + "template_data": [ + { + "name": "color_1", + "lable": "Color 1" + }, + { + "name": "color_2", + "lable": "Color 2" + } + ] + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/the_template.json new file mode 100644 index 0000000000..af8fd9dae4 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/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/example_addons/example_addon/settings/schemas/system_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json new file mode 100644 index 0000000000..0fb0a7c1be --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/schemas/system_schemas/main.json @@ -0,0 +1,14 @@ +{ + "type": "dict", + "key": "example_addon", + "label": "Example addon", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] +} diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py new file mode 100644 index 0000000000..8a74ad859f --- /dev/null +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -0,0 +1,30 @@ +from Qt import QtWidgets + + +class MyExampleDialog(QtWidgets.QDialog): + def __init__(self, parent=None): + super(MyExampleDialog, self).__init__(parent) + + self.setWindowTitle("Connected modules") + + label_widget = QtWidgets.QLabel(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) + + self._label_widget = label_widget + + def set_connected_modules(self, connected_modules): + if connected_modules: + message = "\n".join(connected_modules) + else: + message = ( + "Other enabled modules/addons are not using my interface." + ) + self._label_widget.setText(message) From 8c37b2b1419d1b5e3065a4ebfe42e305165c1273 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:18 +0200 Subject: [PATCH 07/62] fixed settings and widget of example addon --- .../example_addons/example_addon/__init__.py | 2 ++ .../example_addons/example_addon/addon.py | 10 +++++++++- .../settings/defaults/project_settings.json | 16 +++++++++++++++- .../settings/defaults/system_settings.json | 5 +++++ .../dynamic_schemas/project_dynamic_schemas.json | 2 +- .../dynamic_schemas/system_dynamic_schemas.json | 2 +- .../settings/schemas/project_schemas/main.json | 7 ++++--- .../example_addons/example_addon/widgets.py | 9 +++++++++ 8 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json diff --git a/openpype/modules/example_addons/example_addon/__init__.py b/openpype/modules/example_addons/example_addon/__init__.py index df4d61650b..721d924436 100644 --- a/openpype/modules/example_addons/example_addon/__init__.py +++ b/openpype/modules/example_addons/example_addon/__init__.py @@ -6,8 +6,10 @@ be found by OpenPype discovery. from .addon import ( AddonSettingsDef, + ExampleAddon ) __all__ = ( "AddonSettingsDef", + "ExampleAddon" ) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 64504be756..5a25b80616 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -31,7 +31,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): # 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 = "addon_with_settings" + schema_prefix = "example_addon" def get_settings_root_path(self): """Implemented abstract class of JsonFilesSettingsDef. @@ -67,6 +67,14 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): # UI which must not be created at this time self._dialog = None + def tray_init(self): + """Implementation of abstract method for `ITrayAction`. + + We're definetely in trat tool so we can precreate dialog. + """ + + self._create_dialog() + def connect_with_modules(self, enabled_modules): """Method where you should find connected modules. diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json index 0967ef424b..0a01fa8977 100644 --- a/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json +++ b/openpype/modules/example_addons/example_addon/settings/defaults/project_settings.json @@ -1 +1,15 @@ -{} +{ + "project_settings/example_addon": { + "number": 0, + "color_1": [ + 0.0, + 0.0, + 0.0 + ], + "color_2": [ + 0.0, + 0.0, + 0.0 + ] + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json new file mode 100644 index 0000000000..1e77356373 --- /dev/null +++ b/openpype/modules/example_addons/example_addon/settings/defaults/system_settings.json @@ -0,0 +1,5 @@ +{ + "modules/example_addon": { + "enabled": true + } +} \ No newline at end of file diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json index f6b7d5d146..1f3da7b37f 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/project_dynamic_schemas.json @@ -1,6 +1,6 @@ { "project_settings/global": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json index 6895fb8f6d..6faa48ba74 100644 --- a/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json +++ b/openpype/modules/example_addons/example_addon/settings/dynamic_schemas/system_dynamic_schemas.json @@ -1,6 +1,6 @@ { "system_settings/modules": { "type": "schema", - "name": "addon_with_settings/main" + "name": "example_addon/main" } } diff --git a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json index 80e53ace7f..ba692d860e 100644 --- a/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json +++ b/openpype/modules/example_addons/example_addon/settings/schemas/project_schemas/main.json @@ -1,6 +1,7 @@ { "type": "dict", - "key": "exmaple_addon", + "key": "example_addon", + "label": "Example addon", "collapsible": true, "children": [ { @@ -17,11 +18,11 @@ "template_data": [ { "name": "color_1", - "lable": "Color 1" + "label": "Color 1" }, { "name": "color_2", - "lable": "Color 2" + "label": "Color 2" } ] } diff --git a/openpype/modules/example_addons/example_addon/widgets.py b/openpype/modules/example_addons/example_addon/widgets.py index 8a74ad859f..0acf238409 100644 --- a/openpype/modules/example_addons/example_addon/widgets.py +++ b/openpype/modules/example_addons/example_addon/widgets.py @@ -1,5 +1,7 @@ from Qt import QtWidgets +from openpype.style import load_stylesheet + class MyExampleDialog(QtWidgets.QDialog): def __init__(self, parent=None): @@ -18,8 +20,15 @@ class MyExampleDialog(QtWidgets.QDialog): 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) + def set_connected_modules(self, connected_modules): if connected_modules: message = "\n".join(connected_modules) From 4b0c8abcd52cd0254e4df73c314b98a2ec49f2fe Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:11:36 +0200 Subject: [PATCH 08/62] added new dynamic schema with name `system_settings/modules` --- .../entities/schemas/system_schema/schema_modules.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..4287dd7905 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -242,6 +242,10 @@ "label": "Enabled" } ] + }, + { + "type": "dynamic_schema", + "name": "system_settings/modules" } ] } From fa8383859cc0c3d01520a0e6213ff3caf3d65da5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:10 +0200 Subject: [PATCH 09/62] added ability to use schema as first children of dynamic schema definition --- openpype/settings/entities/lib.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/lib.py b/openpype/settings/entities/lib.py index f207322dee..bf3868c08d 100644 --- a/openpype/settings/entities/lib.py +++ b/openpype/settings/entities/lib.py @@ -168,9 +168,13 @@ class SchemasHub: if isinstance(def_schema, dict): def_schema = [def_schema] + all_def_schema = [] for item in def_schema: - item["_dynamic_schema_id"] = def_id - output.extend(def_schema) + items = self.resolve_schema_data(item) + for _item in items: + _item["_dynamic_schema_id"] = def_id + all_def_schema.extend(items) + output.extend(all_def_schema) return output def get_template_name(self, item_def, default=None): From 5bd1fa8a56b41913932df4aeb61a101109892c88 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:12:22 +0200 Subject: [PATCH 10/62] skip OpenPypeAddOn items too --- openpype/modules/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 01c3cebe60..2cd11e5b94 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -495,6 +495,7 @@ class ModulesManager: if ( not inspect.isclass(modules_item) or modules_item is OpenPypeModule + or modules_item is OpenPypeAddOn or not issubclass(modules_item, OpenPypeModule) ): continue From 7f7c7e00620e4a20143b6d1a2d14a51958a5fc9e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 12:19:42 +0200 Subject: [PATCH 11/62] added few more information about addons settings to readme --- openpype/modules/README.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a3733518ac..a6857b2c51 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -10,8 +10,6 @@ OpenPype modules should contain separated logic of specific kind of implementati - add module/addon manifest - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - defying that folder is content of a module or an addon -- module/addon have it's settings schemas and default values outside OpenPype -- add general setting of paths to modules ## Base class `OpenPypeModule` - abstract class as base for each module @@ -25,6 +23,26 @@ OpenPype modules should contain separated logic of specific kind of implementati - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces +## Addon class `OpenPypeAddOn` +- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) + +## How to add addons/modules +- in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder +- for openpype example addons use `{OPENPYPE_REPOS_ROOT}/openpype/modules/example_addons` + +## Addon/module settings +- addons/modules may have defined custom settings definitions with default values +- it is based on settings type `dynamic_schema` which has `name` + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherit from `BaseModuleSettingsDef` + - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons +- settings definition returns schemas by dynamic schemas names + # Interfaces - interface is class that has defined abstract methods to implement and may contain preimplemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized From 0932ea8aba1ec9af50fcbafa87b7022a7ac6dca0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 7 Sep 2021 13:25:13 +0200 Subject: [PATCH 12/62] removed unsused import --- .../example_addon/plugins/publish/example_plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py index 8e7fb410bd..695120e93b 100644 --- a/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py +++ b/openpype/modules/example_addons/example_addon/plugins/publish/example_plugin.py @@ -1,4 +1,3 @@ -import os import pyblish.api From c6d781d7df7ab6e847328fc8188d77e29fedc941 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 13:34:56 +0200 Subject: [PATCH 13/62] #1976 - added methods to get configurable items for providers without use of Settings --- .../sync_server/sync_server_module.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index e65a410551..d2c70ec75a 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,6 +403,59 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) + def get_system_configurable_items_for_provider(self, provider_name): + """ Gets system level configurable items without use of Setting + + Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_project_configurable_items_for_provider(self, provider_name): + """ Gets project level configurable items without use of Setting + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + return self._get_configurable_items_for_provider(provider_name, scope) + + def get_system_configurable_items_for_providers(self): + """ Gets system level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.SYSTEM + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def get_project_configurable_items_for_providers(self): + """ Gets project level configurable items for all providers. + + It is not using Setting! Used for Setting UI to provide forms. + """ + scope = EditableScopes.PROJECT + ret_dict = {} + for provider_name in lib.factory.providers: + ret_dict[provider_name] = \ + self._get_configurable_items_for_provider(provider_name, scope) + + return ret_dict + + def _get_configurable_items_for_provider(self, provider_name, scope): + items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + + for item_key, item in items.items(): + if scope in item["scope"]: + item.pop("scope") + ret_dict[item_key] = item + + return ret_dict + def get_configurable_items(self, scope=None): """ Returns list of sites that could be configurable for all projects. From a4706062d3d345757a4248c2d650b19647553fe4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 14:50:55 +0200 Subject: [PATCH 14/62] #1976 - made new methods class methods --- .../sync_server/sync_server_module.py | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index d2c70ec75a..3e10ddac1d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -403,23 +403,28 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """Wrapper for Local settings - all projects incl. Default""" return self.get_configurable_items(EditableScopes.LOCAL) - def get_system_configurable_items_for_provider(self, provider_name): + @classmethod + def get_system_configurable_items_for_provider(cls, provider_name): """ Gets system level configurable items without use of Setting Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_project_configurable_items_for_provider(self, provider_name): + @classmethod + def get_project_configurable_items_for_provider(cls, provider_name): """ Gets project level configurable items without use of Setting It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return self._get_configurable_items_for_provider(provider_name, scope) + return SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) - def get_system_configurable_items_for_providers(self): + @classmethod + def get_system_configurable_items_for_providers(cls): """ Gets system level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -428,11 +433,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def get_project_configurable_items_for_providers(self): + @classmethod + def get_project_configurable_items_for_providers(cls): """ Gets project level configurable items for all providers. It is not using Setting! Used for Setting UI to provide forms. @@ -441,11 +448,13 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - self._get_configurable_items_for_provider(provider_name, scope) + SyncServerModule._get_configurable_items_for_provider( + provider_name, scope) return ret_dict - def _get_configurable_items_for_provider(self, provider_name, scope): + @classmethod + def _get_configurable_items_for_provider(cls, provider_name, scope): items = lib.factory.get_provider_configurable_items(provider_name) ret_dict = {} From eef59e6fffb67671f752c6aef890c0c5d0ae5255 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:25:54 +0200 Subject: [PATCH 15/62] #1976 - standardize return to list --- .../sync_server/sync_server_module.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 3e10ddac1d..eeff5c499d 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -455,15 +455,24 @@ class SyncServerModule(OpenPypeModule, ITrayModule): @classmethod def _get_configurable_items_for_provider(cls, provider_name, scope): + """ + Args: + provider_name (str) + scope (EditableScopes) + Returns + (list) of (dict) + """ items = lib.factory.get_provider_configurable_items(provider_name) - ret_dict = {} - for item_key, item in items.items(): + ret = [] + for key in sorted(items.keys()): + item = items[key] if scope in item["scope"]: - item.pop("scope") - ret_dict[item_key] = item + item.pop("scope") # unneeded by UI + item.pop("namespace", None) # unneeded by UI + ret.append(item) - return ret_dict + return ret def get_configurable_items(self, scope=None): """ From 39341b1c2bb40caf038b5cb5595efcb1561ee047 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Sep 2021 15:32:14 +0200 Subject: [PATCH 16/62] #1976 - explicitly remove namespace in some cases Used cls instead of class name --- .../sync_server/sync_server_module.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index eeff5c499d..f0ae64d3fd 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -410,8 +410,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Used for Setting UI to provide forms. """ scope = EditableScopes.SYSTEM - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_project_configurable_items_for_provider(cls, provider_name): @@ -420,8 +419,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): It is not using Setting! Used for Setting UI to provide forms. """ scope = EditableScopes.PROJECT - return SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + return cls._get_configurable_items_for_provider(provider_name, scope) @classmethod def get_system_configurable_items_for_providers(cls): @@ -433,8 +431,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -448,8 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): ret_dict = {} for provider_name in lib.factory.providers: ret_dict[provider_name] = \ - SyncServerModule._get_configurable_items_for_provider( - provider_name, scope) + cls._get_configurable_items_for_provider(provider_name, scope) return ret_dict @@ -469,7 +465,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): item = items[key] if scope in item["scope"]: item.pop("scope") # unneeded by UI - item.pop("namespace", None) # unneeded by UI + if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: + item.pop("namespace", None) # unneeded by UI ret.append(item) return ret From 94b3a182ef37d8d58f21026463fa0a00f50442e8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:50:20 +0200 Subject: [PATCH 17/62] #1976 - refactored, created simplified methods Commented out currently unneeded methods, not used anywhere, but logic could be salvaged after Settings will be modified --- .../providers/abstract_provider.py | 28 +- .../sync_server/providers/gdrive.py | 62 ++- .../sync_server/providers/lib.py | 8 + .../sync_server/providers/local_drive.py | 55 ++- .../sync_server/sync_server_module.py | 461 ++++++++---------- 5 files changed, 342 insertions(+), 272 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py index 2e9632134c..7fd25b9852 100644 --- a/openpype/modules/default_modules/sync_server/providers/abstract_provider.py +++ b/openpype/modules/default_modules/sync_server/providers/abstract_provider.py @@ -29,13 +29,35 @@ class AbstractProvider: @classmethod @abc.abstractmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level Returns: - (dict) + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + + @classmethod + @abc.abstractmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + + + Returns: + (list) of dict """ @abc.abstractmethod diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 18d679b833..5db728f2de 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -96,30 +96,62 @@ class GDriveHandler(AbstractProvider): return self.service is not None @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties. + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level Returns: (dict) """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned - editable = { + editable = [ # credentials could be override on Project or User level - 'credentials_url': { - 'scope': [EditableScopes.PROJECT, - EditableScopes.LOCAL], + { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - 'root': {'scope': [EditableScopes.PROJECT], - 'label': "Roots", - 'type': 'dict'} - } + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + } + ] return editable def get_roots_config(self, anatomy=None): diff --git a/openpype/modules/default_modules/sync_server/providers/lib.py b/openpype/modules/default_modules/sync_server/providers/lib.py index 816ccca981..463e49dd4d 100644 --- a/openpype/modules/default_modules/sync_server/providers/lib.py +++ b/openpype/modules/default_modules/sync_server/providers/lib.py @@ -76,6 +76,14 @@ class ProviderFactory: return provider_info[0].get_configurable_items() + def get_provider_cls(self, provider_code): + """ + Returns class object for 'provider_code' to run class methods on. + """ + provider_info = self._get_creator_info(provider_code) + + return provider_info[0] + def _get_creator_info(self, provider): """ Collect all necessary info for provider. Currently only creator diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b80ed44f2..b3482ac1d8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -30,18 +30,59 @@ class LocalDriveHandler(AbstractProvider): return True @classmethod - def get_configurable_items(cls): + def get_system_settings_schema(cls): """ - Returns filtered dict of editable properties + Returns dict for editable properties on system settings level + + + Returns: + (list) of dict + """ + return [] + + @classmethod + def get_project_settings_schema(cls): + """ + Returns dict for editable properties on project settings level + + + Returns: + (list) of dict + """ + # {platform} tells that value is multiplatform and only specific OS + # should be returned + editable = [ + # credentials could be override on Project or User level + { + 'label': "Credentials url", + 'type': 'text', + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' + # noqa: E501 + }, + # roots could be override only on Project leve, User cannot + # + { + 'label': "Roots", + 'type': 'dict' + } + ] + return editable + + @classmethod + def get_local_settings_schema(cls): + """ + Returns dict for editable properties on local settings level + Returns: (dict) """ - editable = { - 'root': {'scope': [EditableScopes.LOCAL], - 'label': "Roots", - 'type': 'dict'} - } + editable = [ + { + 'label': "Roots", + 'type': 'dict' + } + ] return editable def upload_file(self, source_path, target_path, diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index f0ae64d3fd..2eb749801e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype_interfaces import ITrayModule +from openpype.modules.default_modules.interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -399,272 +399,239 @@ class SyncServerModule(OpenPypeModule, ITrayModule): return remote_site - def get_local_settings_schema(self): - """Wrapper for Local settings - all projects incl. Default""" - return self.get_configurable_items(EditableScopes.LOCAL) - + # Methods for Settings UI to draw appropriate forms @classmethod - def get_system_configurable_items_for_provider(cls, provider_name): - """ Gets system level configurable items without use of Setting + def get_system_settings_schema(cls): + """ Gets system level schema of configurable items Used for Setting UI to provide forms. """ - scope = EditableScopes.SYSTEM - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_project_configurable_items_for_provider(cls, provider_name): - """ Gets project level configurable items without use of Setting - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.PROJECT - return cls._get_configurable_items_for_provider(provider_name, scope) - - @classmethod - def get_system_configurable_items_for_providers(cls): - """ Gets system level configurable items for all providers. - - It is not using Setting! Used for Setting UI to provide forms. - """ - scope = EditableScopes.SYSTEM ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_system_settings_schema() return ret_dict @classmethod - def get_project_configurable_items_for_providers(cls): - """ Gets project level configurable items for all providers. + def get_project_settings_schema(cls): + """ Gets project level schema of configurable items. It is not using Setting! Used for Setting UI to provide forms. """ - scope = EditableScopes.PROJECT ret_dict = {} - for provider_name in lib.factory.providers: - ret_dict[provider_name] = \ - cls._get_configurable_items_for_provider(provider_name, scope) + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_project_settings_schema() return ret_dict @classmethod - def _get_configurable_items_for_provider(cls, provider_name, scope): + def get_local_settings_schema(cls): + """ Gets local level schema of configurable items. + + It is not using Setting! Used for Setting UI to provide forms. """ - Args: - provider_name (str) - scope (EditableScopes) - Returns - (list) of (dict) - """ - items = lib.factory.get_provider_configurable_items(provider_name) + ret_dict = {} + for provider_code in lib.factory.providers: + ret_dict[provider_code] = \ + lib.factory.get_provider_cls(provider_code). \ + get_local_settings_schema() - ret = [] - for key in sorted(items.keys()): - item = items[key] - if scope in item["scope"]: - item.pop("scope") # unneeded by UI - if scope in [EditableScopes.SYSTEM, EditableScopes.PROJECT]: - item.pop("namespace", None) # unneeded by UI - ret.append(item) + return ret_dict - return ret - - def get_configurable_items(self, scope=None): - """ - Returns list of sites that could be configurable for all projects. - - Could be filtered by 'scope' argument (list) - - Args: - scope (list of utils.EditableScope) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "value":"{'work': 'c:/projects'}", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - editable = {} - applicable_projects = list(self.connection.projects()) - applicable_projects.append(None) - for project in applicable_projects: - project_name = None - if project: - project_name = project["name"] - - items = self.get_configurable_items_for_project(project_name, - scope) - editable.update(items) - - return editable - - def get_local_settings_schema_for_project(self, project_name): - """Wrapper for Local settings - for specific 'project_name'""" - return self.get_configurable_items_for_project(project_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_project(self, project_name=None, - scope=None): - """ - Returns list of items that could be configurable for specific - 'project_name' - - Args: - project_name (str) - None > default project, - scope (list of utils.EditableScope) - (optional, None is all scopes, default is LOCAL) - - Returns: - (dict of list of dict) - { - siteA : [ - { - key:"root", label:"root", - "type": "dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, - { - key:"credentials_url", label:"Credentials url", - "value":"'c:/projects/cred.json'", "type": "text", - "namespace": "{project_setting}/global/sync_server/ - sites" - } - ] - } - """ - allowed_sites = set() - sites = self.get_all_site_configs(project_name) - if project_name: - # Local Settings can select only from allowed sites for project - allowed_sites.update(set(self.get_active_sites(project_name))) - allowed_sites.update(set(self.get_remote_sites(project_name))) - - editable = {} - for site_name in sites.keys(): - if allowed_sites and site_name not in allowed_sites: - continue - - items = self.get_configurable_items_for_site(project_name, - site_name, - scope) - # Local Settings need 'local' instead of real value - site_name = site_name.replace(get_local_site_id(), 'local') - editable[site_name] = items - - return editable - - def get_local_settings_schema_for_site(self, project_name, site_name): - """Wrapper for Local settings - for particular 'site_name and proj.""" - return self.get_configurable_items_for_site(project_name, - site_name, - EditableScopes.LOCAL) - - def get_configurable_items_for_site(self, project_name=None, - site_name=None, - scope=None): - """ - Returns list of items that could be configurable. - - Args: - project_name (str) - None > default project - site_name (str) - scope (list of utils.EditableScope) - (optional, None is all scopes) - - Returns: - (list) - [ - { - key:"root", label:"root", type:"dict", - "children":[ - { "key": "work", - "type": "text", - "value": "c:/projects"} - ] - }, ... - ] - """ - provider_name = self.get_provider_for_site(site=site_name) - items = lib.factory.get_provider_configurable_items(provider_name) - - if project_name: - sync_s = self.get_sync_project_setting(project_name, - exclude_locals=True, - cached=False) - else: - sync_s = get_default_project_settings(exclude_locals=True) - sync_s = sync_s["global"]["sync_server"] - sync_s["sites"].update( - self._get_default_site_configs(self.enabled)) - - editable = [] - if type(scope) is not list: - scope = [scope] - scope = set(scope) - for key, properties in items.items(): - if scope is None or scope.intersection(set(properties["scope"])): - val = sync_s.get("sites", {}).get(site_name, {}).get(key) - - item = { - "key": key, - "label": properties["label"], - "type": properties["type"] - } - - if properties.get("namespace"): - item["namespace"] = properties.get("namespace") - if "platform" in item["namespace"]: - try: - if val: - val = val[platform.system().lower()] - except KeyError: - st = "{}'s field value {} should be".format(key, val) # noqa: E501 - log.error(st + " multiplatform dict") - - item["namespace"] = item["namespace"].replace('{site}', - site_name) - children = [] - if properties["type"] == "dict": - if val: - for val_key, val_val in val.items(): - child = { - "type": "text", - "key": val_key, - "value": val_val - } - children.append(child) - - if properties["type"] == "dict": - item["children"] = children - else: - item["value"] = val - - editable.append(item) - - return editable + # Needs to be refactored after Settings are updated + # # Methods for Settings to get appriate values to fill forms + # def get_configurable_items(self, scope=None): + # """ + # Returns list of sites that could be configurable for all projects. + # + # Could be filtered by 'scope' argument (list) + # + # Args: + # scope (list of utils.EditableScope) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "value":"{'work': 'c:/projects'}", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # editable = {} + # applicable_projects = list(self.connection.projects()) + # applicable_projects.append(None) + # for project in applicable_projects: + # project_name = None + # if project: + # project_name = project["name"] + # + # items = self.get_configurable_items_for_project(project_name, + # scope) + # editable.update(items) + # + # return editable + # + # def get_local_settings_schema_for_project(self, project_name): + # """Wrapper for Local settings - for specific 'project_name'""" + # return self.get_configurable_items_for_project(project_name, + # EditableScopes.LOCAL) + # + # def get_configurable_items_for_project(self, project_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable for specific + # 'project_name' + # + # Args: + # project_name (str) - None > default project, + # scope (list of utils.EditableScope) + # (optional, None is all scopes, default is LOCAL) + # + # Returns: + # (dict of list of dict) + # { + # siteA : [ + # { + # key:"root", label:"root", + # "type": "dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, + # { + # key:"credentials_url", label:"Credentials url", + # "value":"'c:/projects/cred.json'", "type": "text", + # "namespace": "{project_setting}/global/sync_server/ + # sites" + # } + # ] + # } + # """ + # allowed_sites = set() + # sites = self.get_all_site_configs(project_name) + # if project_name: + # # Local Settings can select only from allowed sites for project + # allowed_sites.update(set(self.get_active_sites(project_name))) + # allowed_sites.update(set(self.get_remote_sites(project_name))) + # + # editable = {} + # for site_name in sites.keys(): + # if allowed_sites and site_name not in allowed_sites: + # continue + # + # items = self.get_configurable_items_for_site(project_name, + # site_name, + # scope) + # # Local Settings need 'local' instead of real value + # site_name = site_name.replace(get_local_site_id(), 'local') + # editable[site_name] = items + # + # return editable + # + # def get_configurable_items_for_site(self, project_name=None, + # site_name=None, + # scope=None): + # """ + # Returns list of items that could be configurable. + # + # Args: + # project_name (str) - None > default project + # site_name (str) + # scope (list of utils.EditableScope) + # (optional, None is all scopes) + # + # Returns: + # (list) + # [ + # { + # key:"root", label:"root", type:"dict", + # "children":[ + # { "key": "work", + # "type": "text", + # "value": "c:/projects"} + # ] + # }, ... + # ] + # """ + # provider_name = self.get_provider_for_site(site=site_name) + # items = lib.factory.get_provider_configurable_items(provider_name) + # + # if project_name: + # sync_s = self.get_sync_project_setting(project_name, + # exclude_locals=True, + # cached=False) + # else: + # sync_s = get_default_project_settings(exclude_locals=True) + # sync_s = sync_s["global"]["sync_server"] + # sync_s["sites"].update( + # self._get_default_site_configs(self.enabled)) + # + # editable = [] + # if type(scope) is not list: + # scope = [scope] + # scope = set(scope) + # for key, properties in items.items(): + # if scope is None or scope.intersection(set(properties["scope"])): + # val = sync_s.get("sites", {}).get(site_name, {}).get(key) + # + # item = { + # "key": key, + # "label": properties["label"], + # "type": properties["type"] + # } + # + # if properties.get("namespace"): + # item["namespace"] = properties.get("namespace") + # if "platform" in item["namespace"]: + # try: + # if val: + # val = val[platform.system().lower()] + # except KeyError: + # st = "{}'s field value {} should be".format(key, val) # noqa: E501 + # log.error(st + " multiplatform dict") + # + # item["namespace"] = item["namespace"].replace('{site}', + # site_name) + # children = [] + # if properties["type"] == "dict": + # if val: + # for val_key, val_val in val.items(): + # child = { + # "type": "text", + # "key": val_key, + # "value": val_val + # } + # children.append(child) + # + # if properties["type"] == "dict": + # item["children"] = children + # else: + # item["value"] = val + # + # editable.append(item) + # + # return editable def reset_timer(self): """ From 3f2a4d5a5aa5f8d037d55551a127e86990b60c70 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Sep 2021 11:59:40 +0200 Subject: [PATCH 18/62] Hound --- .../default_modules/sync_server/sync_server_module.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 2eb749801e..39b5c9314e 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -8,7 +8,7 @@ import copy from avalon.api import AvalonMongoDB from openpype.modules import OpenPypeModule -from openpype.modules.default_modules.interfaces import ITrayModule +from openpype_interfaces import ITrayModule from openpype.api import ( Anatomy, get_project_settings, @@ -16,14 +16,13 @@ from openpype.api import ( get_local_site_id) from openpype.lib import PypeLogger from openpype.settings.lib import ( - get_default_project_settings, get_default_anatomy_settings, get_anatomy_settings) from .providers.local_drive import LocalDriveHandler from .providers import lib -from .utils import time_function, SyncStatus, EditableScopes +from .utils import time_function, SyncStatus log = PypeLogger().get_logger("SyncServer") @@ -646,7 +645,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(): + for project in self.connection.projects(projection={"name": 1}): project_name = project["name"] project_settings = self.get_sync_project_setting(project_name) if project_settings and project_settings.get("enabled"): From 7d37971d64f57408b22fc582e1a388a99cc28c3a Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 15:47:38 +0200 Subject: [PATCH 19/62] get last version string from path This changes get_version_from_path() to produce the same result as version_up --- openpype/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index e1dd1e7f10..88bb1a216a 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -77,7 +77,7 @@ def get_version_from_path(file): """ pattern = re.compile(r"[\._]v([0-9]+)", re.IGNORECASE) try: - return pattern.findall(file)[0] + return pattern.findall(file)[-1] except IndexError: log.error( "templates:get_version_from_workfile:" From 7a88a4ac1326796b6224e5547a3656b2a5b9032c Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 16:11:30 +0200 Subject: [PATCH 20/62] Makes thumbnail from the middle of the clip If read node frame range is 1001-1010, thumbnail is now made from frame 1005, not 505. --- .../nuke/plugins/publish/extract_thumbnail.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index da30dcc632..6921d6e9b3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -112,12 +112,12 @@ class ExtractThumbnail(openpype.api.Extractor): # create write node write_node = nuke.createNode("Write") - file = fhead + "jpeg" + file = fhead + "jpg" name = "thumbnail" path = os.path.join(staging_dir, file).replace("\\", "/") instance.data["thumbnail"] = path write_node["file"].setValue(path) - write_node["file_type"].setValue("jpeg") + write_node["file_type"].setValue("jpg") write_node["raw"].setValue(1) write_node.setInput(0, previous_node) temporary_nodes.append(write_node) @@ -126,10 +126,11 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 + mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) repre = { 'name': name, - 'ext': "jpeg", + 'ext': "jpg", "outputName": "thumb", 'files': file, "stagingDir": staging_dir, @@ -140,7 +141,7 @@ class ExtractThumbnail(openpype.api.Extractor): instance.data["representations"].append(repre) # Render frames - nuke.execute(write_node.name(), int(first_frame), int(last_frame)) + nuke.execute(write_node.name(), int(mid_frame), int(mid_frame)) self.log.debug( "representations: {}".format(instance.data["representations"])) @@ -157,12 +158,12 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + if "Viewer" == n.Class()]: + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 0518064e96ef6b3a714988dfe88c6061173795f2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:30:17 +0200 Subject: [PATCH 21/62] indent change --- .../hosts/nuke/plugins/publish/extract_thumbnail.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 6921d6e9b3..93a5c9b51d 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,11 +159,11 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() if "Viewer" == n.Class()]: - ip = v['input_process'].getValue() - ipn = v['input_process_node'].getValue() - if "VIEWER_INPUT" not in ipn and ip: - ipn_orig = nuke.toNode(ipn) - ipn_orig.setSelected(True) + ip = v['input_process'].getValue() + ipn = v['input_process_node'].getValue() + if "VIEWER_INPUT" not in ipn and ip: + ipn_orig = nuke.toNode(ipn) + ipn_orig.setSelected(True) if ipn_orig: nuke.nodeCopy('%clipboard%') From 81432101164a3731c65e6d60b9880029a28e8202 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:33:32 +0200 Subject: [PATCH 22/62] long line fix --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 93a5c9b51d..dfb26aab80 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,7 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame) - int(first_frame) ) / 2) + int(first_frame) + mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) repre = { 'name': name, From 25a7c5d0f0d15525d1fe58ffdc8f120558eac462 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:38:29 +0200 Subject: [PATCH 23/62] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index dfb26aab80..4c21891f48 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -126,7 +126,8 @@ class ExtractThumbnail(openpype.api.Extractor): # retime for first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 - mid_frame = int((int(last_frame)-int(first_frame))/2)+int(first_frame) + mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + + int(first_frame) repre = { 'name': name, From 17fd666a49a374a895a4c6913cb8f326c411de56 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:40:58 +0200 Subject: [PATCH 24/62] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 4c21891f48..c0dc1417c3 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -127,7 +127,7 @@ class ExtractThumbnail(openpype.api.Extractor): first_frame = int(last_frame) / 2 last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ - + int(first_frame) + + int(first_frame) repre = { 'name': name, From 1ab40ddd244b27db31fe639cc63e15096301e1e9 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:43:31 +0200 Subject: [PATCH 25/62] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index c0dc1417c3..7ffeec2db9 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -124,10 +124,10 @@ class ExtractThumbnail(openpype.api.Extractor): tags = ["thumbnail", "publish_on_farm"] # retime for - first_frame = int(last_frame) / 2 - last_frame = int(last_frame) / 2 mid_frame = int((int(last_frame) - int(first_frame)) / 2) \ + int(first_frame) + first_frame = int(last_frame) / 2 + last_frame = int(last_frame) / 2 repre = { 'name': name, From ca3034f2725fb1be97b37850f8a7d1ab0e017be7 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:46:09 +0200 Subject: [PATCH 26/62] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 7ffeec2db9..b9d6762880 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From e3b319ffc66fc24668480fc3405bb05aa51089c2 Mon Sep 17 00:00:00 2001 From: jrsndlr Date: Wed, 8 Sep 2021 17:58:31 +0200 Subject: [PATCH 27/62] style --- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index b9d6762880..55f7b746fc 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -159,7 +159,7 @@ class ExtractThumbnail(openpype.api.Extractor): ipn_orig = None for v in [n for n in nuke.allNodes() - if "Viewer" == n.Class()]: + if "Viewer" == n.Class()]: ip = v['input_process'].getValue() ipn = v['input_process_node'].getValue() if "VIEWER_INPUT" not in ipn and ip: From 46f38b1e02888769f93727ae77baedc6182c5c26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:18:02 +0200 Subject: [PATCH 28/62] Fix typos --- openpype/modules/README.md | 168 ++++++++++++++++++------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index a6857b2c51..abc7ed3961 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -1,31 +1,31 @@ # OpenPype modules/addons -OpenPype modules should contain separated logic of specific kind of implementation, like Ftrack connection and usage code or Deadline farm rendering or may contain only special plugins. Addons work the same way currently there is no difference in module and addon. +OpenPype modules should contain separated logic of specific kind of implementation, such as Ftrack connection and its usage code, Deadline farm rendering or may contain only special plugins. Addons work the same way currently, there is no difference between module and addon functionality. ## Modules concept -- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the modulo located -- modules or addons should never be imported directly even if you know possible full import path - - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts +- modules and addons are dynamically imported to virtual python module `openpype_modules` from which it is possible to import them no matter where is the module located +- modules or addons should never be imported directly, even if you know possible full import path + - it is because all of their content must be imported in specific order and should not be imported without defined functions as it may also break few implementation parts ### TODOs - add module/addon manifest - - definition of module (not 100% defined content e.g. minimum require OpenPype version etc.) - - defying that folder is content of a module or an addon + - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) + - defying that folder is content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module -- implementation should be module's api withou GUI parts -- may implement `get_global_environments` method which should return dictionary of environments that are globally appliable and value is the same for whole studio if launched at any workstation (except os specific paths) +- implementation should contain module's api without GUI parts +- may implement `get_global_environments` method which should return dictionary of environments that are globally applicable and value is the same for whole studio if launched at any workstation (except os specific paths) - abstract parts: - - `name` attribute - name of a module - - `initialize` method - method for own initialization of a module (should not override `__init__`) - - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules -- `__init__` should not be overriden and `initialize` should not do time consuming part but only prepare base data about module - - also keep in mind that they may be initialized in headless mode + - `name` attribute - name of a module + - `initialize` method - method for own initialization of a module (should not override `__init__`) + - `connect_with_modules` method - where module may look for it's interfaces implementations or check for other modules +- `__init__` should not be overridden and `initialize` should not do time consuming part but only prepare base data about module + - also keep in mind that they may be initialized in headless mode - connection with other modules is made with help of interfaces ## Addon class `OpenPypeAddOn` -- inherit from `OpenPypeModule` but is enabled by default and don't have to implement `initialize` and `connect_with_modules` methods - - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) +- inherits from `OpenPypeModule` but is enabled by default and doesn't have to implement `initialize` and `connect_with_modules` methods + - that is because it is expected that addons don't need to have system settings and `enabled` value on it (but it is possible...) ## How to add addons/modules - in System settings go to `modules/addon_paths` (`Modules/OpenPype AddOn Paths`) where you have to add path to addon root folder @@ -34,110 +34,110 @@ OpenPype modules should contain separated logic of specific kind of implementati ## Addon/module settings - addons/modules may have defined custom settings definitions with default values - it is based on settings type `dynamic_schema` which has `name` - - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults - - they can't be added to any schema hierarchy - - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) - - addons may define it's dynamic schema items -- they can be defined with class which inherit from `BaseModuleSettingsDef` - - it is recommended to use preimplemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values - - check it's docstring and check for `example_addon` in example addons + - that item defines that it can be replaced dynamically with any schemas from module or module which won't be saved to openpype core defaults + - they can't be added to any schema hierarchy + - item must not be in settings group (under overrides) or in dynamic item (e.g. `list` of `dict-modifiable`) + - addons may define it's dynamic schema items +- they can be defined with class which inherits from `BaseModuleSettingsDef` + - it is recommended to use pre implemented `JsonFilesSettingsDef` which defined structure and use json files to define dynamic schemas, schemas and default values + - check it's docstring and check for `example_addon` in example addons - settings definition returns schemas by dynamic schemas names # Interfaces -- interface is class that has defined abstract methods to implement and may contain preimplemented helper methods +- interface is class that has defined abstract methods to implement and may contain pre implemented helper methods - module that inherit from an interface must implement those abstract methods otherwise won't be initialized -- it is easy to find which module object inherited from which interfaces withh 100% chance they have implemented required methods +- it is easy to find which module object inherited from which interfaces with 100% chance they have implemented required methods - interfaces can be defined in `interfaces.py` inside module directory - - the file can't use relative imports or import anything from other parts - of module itself at the header of file - - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation + - the file can't use relative imports or import anything from other parts + of module itself at the header of file + - this is one of reasons why modules/addons can't be imported directly without using defined functions in OpenPype modules implementation ## Base class `OpenPypeInterface` - has nothing implemented - has ABCMeta as metaclass - is defined to be able find out classes which inherit from this base to be - able tell this is an Interface + able tell this is an Interface ## Global interfaces - few interfaces are implemented for global usage ### IPluginPaths -- module want to add directory path/s to avalon or publish plugins +- module wants to add directory path/s to avalon or publish plugins - module must implement `get_plugin_paths` which must return dictionary with possible keys `"publish"`, `"load"`, `"create"` or `"actions"` - - each key may contain list or string with path to directory with plugins + - each key may contain list or string with a path to directory with plugins ### ITrayModule -- module has more logic when used in tray - - it is possible that module can be used only in tray +- module has more logic when used in a tray + - it is possible that module can be used only in the tray - abstract methods - - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` - - `tray_menu` - add actions to tray widget's menu that represent the module - - `tray_start` - start of module's login in tray - - module is initialized and connected with other modules - - `tray_exit` - module's cleanup like stop and join threads etc. - - order of calling is based on implementation this order is how it works with `TrayModulesManager` - - it is recommended to import and use GUI implementaion only in these methods + - `tray_init` - initialization triggered after `initialize` when used in `TrayModulesManager` and before `connect_with_modules` + - `tray_menu` - add actions to tray widget's menu that represent the module + - `tray_start` - start of module's login in tray + - module is initialized and connected with other modules + - `tray_exit` - module's cleanup like stop and join threads etc. + - order of calling is based on implementation this order is how it works with `TrayModulesManager` + - it is recommended to import and use GUI implementation only in these methods - has attribute `tray_initialized` (bool) which is set to False by default and is set by `TrayModulesManager` to True after `tray_init` - - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations + - if module has logic only in tray or for both then should be checking for `tray_initialized` attribute to decide how should handle situations ### ITrayService -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to submenu "Services" in tray widget menu with icon and label -- abstract atttribute `label` - - label shown in menu -- interface has preimplemented methods to change icon color - - `set_service_running` - green icon - - `set_service_failed` - red icon - - `set_service_idle` - orange icon - - these states must be set by module itself `set_service_running` is default state on initialization +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to submenu "Services" in tray widget menu with icon and label +- abstract attribute `label` + - label shown in menu +- interface has pre implemented methods to change icon color + - `set_service_running` - green icon + - `set_service_failed` - red icon + - `set_service_idle` - orange icon + - these states must be set by module itself `set_service_running` is default state on initialization ### ITrayAction -- inherit from `ITrayModule` and implement `tray_menu` method for you - - add action to tray widget menu with label -- abstract atttribute `label` - - label shown in menu +- inherits from `ITrayModule` and implements `tray_menu` method for you + - adds action to tray widget menu with label +- abstract attribute `label` + - label shown in menu - abstract method `on_action_trigger` - - what should happen when action is triggered -- NOTE: It is good idea to implement logic in `on_action_trigger` to api method and trigger that methods on callbacks this gives ability to trigger that method outside tray + - what should happen when an action is triggered +- NOTE: It is a good idea to implement logic in `on_action_trigger` to the api method and trigger that method on callbacks. This gives ability to trigger that method outside tray ## Modules interfaces -- modules may have defined their interfaces to be able recognize other modules that would want to use their features -- -### Example: -- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which of other modules want to add paths to server/user event handlers - - Clockify module use `IFtrackEventHandlerPaths` and return paths to clockify ftrack synchronizers +- modules may have defined their own interfaces to be able to recognize other modules that would want to use their features -- Clockify has more inharitance it's class definition looks like +### Example: +- Ftrack module has `IFtrackEventHandlerPaths` which helps to tell Ftrack module which other modules want to add paths to server/user event handlers + - Clockify module use `IFtrackEventHandlerPaths` and returns paths to clockify ftrack synchronizers + +- Clockify inherits from more interfaces. It's class definition looks like: ``` class ClockifyModule( - OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. - ITrayModule, # Says has special implementation when used in tray. - IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). - IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. - ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. + OpenPypeModule, # Says it's Pype module so ModulesManager will try to initialize. + ITrayModule, # Says has special implementation when used in tray. + IPluginPaths, # Says has plugin paths that want to register (paths to clockify actions for launcher). + IFtrackEventHandlerPaths, # Says has Ftrack actions/events for user/server. + ITimersManager # Listen to other modules with timer and can trigger changes in other module timers through `TimerManager` module. ): ``` ### ModulesManager -- collect module classes and tries to initialize them +- collects module classes and tries to initialize them - important attributes - - `modules` - list of available attributes - - `modules_by_id` - dictionary of modules mapped by their ids - - `modules_by_name` - dictionary of modules mapped by their names - - all these attributes contain all found modules even if are not enabled + - `modules` - list of available attributes + - `modules_by_id` - dictionary of modules mapped by their ids + - `modules_by_name` - dictionary of modules mapped by their names + - all these attributes contain all found modules even if are not enabled - helper methods - - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them - - `collect_plugin_paths` collect plugin paths from all enabled modules - - output is always dictionary with all keys and values as list - ``` - { - "publish": [], - "create": [], - "load": [], - "actions": [] - } - ``` + - `collect_global_environments` to collect all global environments from enabled modules with calling `get_global_environments` on each of them + - `collect_plugin_paths` collects plugin paths from all enabled modules + - output is always dictionary with all keys and values as an list + ``` + { + "publish": [], + "create": [], + "load": [], + "actions": [] + } + ``` ### TrayModulesManager -- inherit from `ModulesManager` -- has specific implementations for Pype Tray tool and handle `ITrayModule` methods +- inherits from `ModulesManager` +- has specific implementation for Pype Tray tool and handle `ITrayModule` methods \ No newline at end of file From a7932ff6d536fb00415158b617d59a348afaa455 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:38:15 +0200 Subject: [PATCH 29/62] Fix typos --- .../modules/example_addons/example_addon/addon.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/modules/example_addons/example_addon/addon.py b/openpype/modules/example_addons/example_addon/addon.py index 5a25b80616..5573e33cc1 100644 --- a/openpype/modules/example_addons/example_addon/addon.py +++ b/openpype/modules/example_addons/example_addon/addon.py @@ -21,11 +21,11 @@ from openpype_interfaces import ( ) -# Settings definiton of this addon using `JsonFilesSettingsDef` -# - JsonFilesSettingsDef is prepared settings definiton using json files -# to define settings and store defaul values +# 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 prefix to every schema and template from `schemas` + # 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 @@ -48,7 +48,7 @@ class AddonSettingsDef(JsonFilesSettingsDef): class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """This Addon has defined it's settings and interface. - This example has system settings with enabled option. And use + 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 @@ -70,7 +70,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): def tray_init(self): """Implementation of abstract method for `ITrayAction`. - We're definetely in trat tool so we can precreate dialog. + We're definitely in tray tool so we can pre create dialog. """ self._create_dialog() @@ -101,7 +101,7 @@ class ExampleAddon(OpenPypeAddOn, IPluginPaths, ITrayAction): """Show dialog with connected modules. This can be called from anywhere but can also crash in headless mode. - There is not way how to prevent addon to do invalid operations if he's + There is no way to prevent addon to do invalid operations if he's not handling them. """ # Make sure dialog is created From 3643b8e1bd7936dd840b343f9b70946600e249d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:43:43 +0200 Subject: [PATCH 30/62] Hound --- .../modules/default_modules/sync_server/providers/gdrive.py | 3 +-- .../default_modules/sync_server/providers/local_drive.py | 3 +-- .../default_modules/sync_server/sync_server_module.py | 6 +++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 5db728f2de..da54eecb8e 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index b3482ac1d8..9678d38ed8 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -56,8 +56,7 @@ class LocalDriveHandler(AbstractProvider): { 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/sync_server_module.py b/openpype/modules/default_modules/sync_server/sync_server_module.py index 39b5c9314e..976a349bfa 100644 --- a/openpype/modules/default_modules/sync_server/sync_server_module.py +++ b/openpype/modules/default_modules/sync_server/sync_server_module.py @@ -445,7 +445,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # # Methods for Settings to get appriate values to fill forms # def get_configurable_items(self, scope=None): # """ - # Returns list of sites that could be configurable for all projects. + # Returns list of sites that could be configurable for all projects # # Could be filtered by 'scope' argument (list) # @@ -468,8 +468,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): # }, # { # key:"credentials_url", label:"Credentials url", - # "value":"'c:/projects/cred.json'", "type": "text", - # "namespace": "{project_setting}/global/sync_server/ + # "value":"'c:/projects/cred.json'", "type": "text", # noqa: E501 + # "namespace": "{project_setting}/global/sync_server/ # noqa: E501 # sites" # } # ] From 2230d60ff407f033ab7127d5f03c3b62e0309e80 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:48:28 +0200 Subject: [PATCH 31/62] #1976 - added 'key' --- .../default_modules/sync_server/providers/gdrive.py | 3 +++ .../sync_server/providers/local_drive.py | 13 +++---------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index da54eecb8e..3bfd6f4854 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -120,6 +120,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 @@ -127,6 +128,7 @@ class GDriveHandler(AbstractProvider): # roots could be override only on Project leve, User cannot # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -145,6 +147,7 @@ class GDriveHandler(AbstractProvider): editable = [ # credentials could be override on Project or User level { + 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 9678d38ed8..4b703267d5 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -49,18 +49,10 @@ class LocalDriveHandler(AbstractProvider): Returns: (list) of dict """ - # {platform} tells that value is multiplatform and only specific OS - # should be returned + # for non 'studio' sites, 'studio' is configured in Anatomy editable = [ - # credentials could be override on Project or User level - { - 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 - }, - # roots could be override only on Project leve, User cannot - # { + 'key': "roots", 'label': "Roots", 'type': 'dict' } @@ -78,6 +70,7 @@ class LocalDriveHandler(AbstractProvider): """ editable = [ { + 'key': "roots", 'label': "Roots", 'type': 'dict' } From fc0872a99750e0c648f7b5c77bb1de65c753a924 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:52:25 +0200 Subject: [PATCH 32/62] #1976 - small fixes --- .../modules/default_modules/sync_server/providers/gdrive.py | 5 ++--- .../default_modules/sync_server/providers/local_drive.py | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 3bfd6f4854..8c93f41d67 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -8,7 +8,7 @@ import platform from openpype.api import Logger from openpype.api import get_system_settings from .abstract_provider import AbstractProvider -from ..utils import time_function, ResumableError, EditableScopes +from ..utils import time_function, ResumableError log = Logger().get_logger("SyncServer") @@ -122,8 +122,7 @@ class GDriveHandler(AbstractProvider): { 'key': "credentials_url", 'label': "Credentials url", - 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 + 'type': 'text' }, # roots could be override only on Project leve, User cannot # diff --git a/openpype/modules/default_modules/sync_server/providers/local_drive.py b/openpype/modules/default_modules/sync_server/providers/local_drive.py index 4b703267d5..8e5f170bc9 100644 --- a/openpype/modules/default_modules/sync_server/providers/local_drive.py +++ b/openpype/modules/default_modules/sync_server/providers/local_drive.py @@ -7,8 +7,6 @@ import time from openpype.api import Logger, Anatomy from .abstract_provider import AbstractProvider -from ..utils import EditableScopes - log = Logger().get_logger("SyncServer") From a5bbe779fbec6fb061234072017aeb212630a594 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 9 Sep 2021 12:56:43 +0200 Subject: [PATCH 33/62] Hound --- .../default_modules/sync_server/providers/gdrive.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/modules/default_modules/sync_server/providers/gdrive.py b/openpype/modules/default_modules/sync_server/providers/gdrive.py index 8c93f41d67..f1ec0b6a0d 100644 --- a/openpype/modules/default_modules/sync_server/providers/gdrive.py +++ b/openpype/modules/default_modules/sync_server/providers/gdrive.py @@ -118,14 +118,13 @@ class GDriveHandler(AbstractProvider): # {platform} tells that value is multiplatform and only specific OS # should be returned editable = [ - # credentials could be override on Project or User level - { + # credentials could be overriden on Project or User level + { 'key': "credentials_url", 'label': "Credentials url", 'type': 'text' }, - # roots could be override only on Project leve, User cannot - # + # roots could be overriden only on Project leve, User cannot { 'key': "roots", 'label': "Roots", @@ -149,8 +148,7 @@ class GDriveHandler(AbstractProvider): 'key': "credentials_url", 'label': "Credentials url", 'type': 'text', - 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' - # noqa: E501 + 'namespace': '{project_settings}/global/sync_server/sites/{site}/credentials_url/{platform}' # noqa: E501 } ] return editable From 72b2f44fa9e829925858e70d621d8622f60d1b5b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:25:58 +0200 Subject: [PATCH 34/62] added loading of steps from schema --- openpype/settings/entities/input_entities.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index ebc70b840d..128625619a 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,6 +379,10 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) + steps = self.schema_data.get("steps", None) + if steps is None: + steps = 1 / (10 ** self.decimal) + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 7e973a5de1fba402a04d1e780a4a6a35e78c8afc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:27:32 +0200 Subject: [PATCH 35/62] added steps to Number widget --- openpype/tools/settings/settings/widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index b821c3bb2c..2caf8c33ba 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -92,11 +92,15 @@ class NumberSpinBox(QtWidgets.QDoubleSpinBox): min_value = kwargs.pop("minimum", -99999) max_value = kwargs.pop("maximum", 99999) decimals = kwargs.pop("decimal", 0) + steps = kwargs.pop("steps", None) + super(NumberSpinBox, self).__init__(*args, **kwargs) self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setDecimals(decimals) self.setMinimum(min_value) self.setMaximum(max_value) + if steps is not None: + self.setSingleStep(steps) def focusInEvent(self, event): super(NumberSpinBox, self).focusInEvent(event) From 7af864a6e0eeb256177525785707d66cc385495c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:28:17 +0200 Subject: [PATCH 36/62] steps can not be set --- openpype/settings/entities/input_entities.py | 5 +---- openpype/tools/settings/settings/item_widgets.py | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 128625619a..4afa0d9484 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,10 +379,7 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - steps = self.schema_data.get("steps", None) - if steps is None: - steps = 1 / (10 ** self.decimal) - self.steps = steps + self.steps = self.schema_data.get("steps", None) def _convert_to_valid_type(self, value): if isinstance(value, str): diff --git a/openpype/tools/settings/settings/item_widgets.py b/openpype/tools/settings/settings/item_widgets.py index 736ba77652..da74c2adc5 100644 --- a/openpype/tools/settings/settings/item_widgets.py +++ b/openpype/tools/settings/settings/item_widgets.py @@ -411,7 +411,8 @@ class NumberWidget(InputWidget): kwargs = { "minimum": self.entity.minimum, "maximum": self.entity.maximum, - "decimal": self.entity.decimal + "decimal": self.entity.decimal, + "steps": self.entity.steps } self.input_field = NumberSpinBox(self.content_widget, **kwargs) input_field_stretch = 1 @@ -426,6 +427,10 @@ class NumberWidget(InputWidget): int(self.entity.minimum * slider_multiplier), int(self.entity.maximum * slider_multiplier) ) + if self.entity.steps is not None: + slider_widget.setSingleStep( + self.entity.steps * slider_multiplier + ) self.content_layout.addWidget(slider_widget, 1) From e3b8e25a1a15cbdc24519e49f0067126bb071610 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:09 +0200 Subject: [PATCH 37/62] added steps to readme --- openpype/settings/entities/schemas/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/entities/schemas/README.md b/openpype/settings/entities/schemas/README.md index 9b53e89dd7..c8432f0f2e 100644 --- a/openpype/settings/entities/schemas/README.md +++ b/openpype/settings/entities/schemas/README.md @@ -316,6 +316,7 @@ How output of the schema could look like on save: - key `"decimal"` defines how many decimal places will be used, 0 is for integer input (Default: `0`) - key `"minimum"` as minimum allowed number to enter (Default: `-99999`) - key `"maxium"` as maximum allowed number to enter (Default: `99999`) +- key `"steps"` will change single step value of UI inputs (using arrows and wheel scroll) - for UI it is possible to show slider to enable this option set `show_slider` to `true` ``` { From e75e9a6465c59751ffb62ac143532255eef9a837 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:33:19 +0200 Subject: [PATCH 38/62] make sure that steps are not `0` --- openpype/settings/entities/input_entities.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/input_entities.py b/openpype/settings/entities/input_entities.py index 4afa0d9484..0ded3ab7e5 100644 --- a/openpype/settings/entities/input_entities.py +++ b/openpype/settings/entities/input_entities.py @@ -379,7 +379,11 @@ class NumberEntity(InputEntity): # UI specific attributes self.show_slider = self.schema_data.get("show_slider", False) - self.steps = self.schema_data.get("steps", None) + steps = self.schema_data.get("steps", None) + # Make sure that steps are not set to `0` + if steps == 0: + steps = None + self.steps = steps def _convert_to_valid_type(self, value): if isinstance(value, str): From 40a6712384e5fec86bd0ab1ccdae2ec8d8317fad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 14:35:19 +0200 Subject: [PATCH 39/62] added steps to avalon mongo timeout --- .../entities/schemas/system_schema/schema_modules.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..b52a646954 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -28,7 +28,8 @@ "type": "number", "key": "AVALON_TIMEOUT", "minimum": 0, - "label": "Avalon Mongo Timeout (ms)" + "label": "Avalon Mongo Timeout (ms)", + "steps": 100 }, { "type": "path", From d961e0a26209fa13da5c184d68be765bfca7c956 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 9 Sep 2021 15:17:23 +0200 Subject: [PATCH 40/62] replaced `providers-enum` with `sync-server-providers` to be able set system settings for provider --- openpype/settings/entities/__init__.py | 8 ++-- .../settings/entities/dict_conditional.py | 46 +++++++++++++++++++ openpype/settings/entities/enum_entity.py | 38 --------------- .../schemas/system_schema/schema_modules.json | 9 +--- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/openpype/settings/entities/__init__.py b/openpype/settings/entities/__init__.py index 8c30d5044c..aae2d1fa89 100644 --- a/openpype/settings/entities/__init__.py +++ b/openpype/settings/entities/__init__.py @@ -105,7 +105,6 @@ from .enum_entity import ( AppsEnumEntity, ToolsEnumEntity, TaskTypeEnumEntity, - ProvidersEnum, DeadlineUrlEnumEntity, AnatomyTemplatesEnumEntity ) @@ -113,7 +112,10 @@ from .enum_entity import ( from .list_entity import ListEntity from .dict_immutable_keys_entity import DictImmutableKeysEntity from .dict_mutable_keys_entity import DictMutableKeysEntity -from .dict_conditional import DictConditionalEntity +from .dict_conditional import ( + DictConditionalEntity, + SyncServerProviders +) from .anatomy_entities import AnatomyEntity @@ -161,7 +163,6 @@ __all__ = ( "AppsEnumEntity", "ToolsEnumEntity", "TaskTypeEnumEntity", - "ProvidersEnum", "DeadlineUrlEnumEntity", "AnatomyTemplatesEnumEntity", @@ -172,6 +173,7 @@ __all__ = ( "DictMutableKeysEntity", "DictConditionalEntity", + "SyncServerProviders", "AnatomyEntity" ) diff --git a/openpype/settings/entities/dict_conditional.py b/openpype/settings/entities/dict_conditional.py index d7b416921c..6f27760570 100644 --- a/openpype/settings/entities/dict_conditional.py +++ b/openpype/settings/entities/dict_conditional.py @@ -724,3 +724,49 @@ class DictConditionalEntity(ItemEntity): for children in self.children.values(): for child_entity in children: child_entity.reset_callbacks() + + +class SyncServerProviders(DictConditionalEntity): + schema_types = ["sync-server-providers"] + + def _add_children(self): + self.enum_key = "provider" + self.enum_label = "Provider" + + enum_children = self._get_enum_children() + if not enum_children: + enum_children.append({ + "key": None, + "label": "< Nothing >" + }) + self.enum_children = enum_children + + super(SyncServerProviders, self)._add_children() + + def _get_enum_children(self): + from openpype_modules import sync_server + + from openpype_modules.sync_server.providers import lib as lib_providers + + provider_code_to_label = {} + providers = lib_providers.factory.providers + for provider_code, provider_info in providers.items(): + provider, _ = provider_info + provider_code_to_label[provider_code] = provider.LABEL + + system_settings_schema = ( + sync_server + .SyncServerModule + .get_system_settings_schema() + ) + + enum_children = [] + for provider_code, configurables in system_settings_schema.items(): + label = provider_code_to_label.get(provider_code) or provider_code + + enum_children.append({ + "key": provider_code, + "label": label, + "children": configurables + }) + return enum_children diff --git a/openpype/settings/entities/enum_entity.py b/openpype/settings/entities/enum_entity.py index cb532c5ae0..66279f529d 100644 --- a/openpype/settings/entities/enum_entity.py +++ b/openpype/settings/entities/enum_entity.py @@ -407,44 +407,6 @@ class TaskTypeEnumEntity(BaseEnumEntity): self._current_value = new_value -class ProvidersEnum(BaseEnumEntity): - schema_types = ["providers-enum"] - - def _item_initalization(self): - self.multiselection = False - self.value_on_not_set = "" - self.enum_items = [] - self.valid_keys = set() - self.valid_value_types = (str, ) - self.placeholder = None - - def _get_enum_values(self): - from openpype_modules.sync_server.providers import lib as lib_providers - - providers = lib_providers.factory.providers - - valid_keys = set() - valid_keys.add('') - enum_items = [{'': 'Choose Provider'}] - for provider_code, provider_info in providers.items(): - provider, _ = provider_info - enum_items.append({provider_code: provider.LABEL}) - valid_keys.add(provider_code) - - return enum_items, valid_keys - - def set_override_state(self, *args, **kwargs): - super(ProvidersEnum, self).set_override_state(*args, **kwargs) - - self.enum_items, self.valid_keys = self._get_enum_values() - - value_on_not_set = list(self.valid_keys)[0] - if self._current_value is NOT_SET: - self._current_value = value_on_not_set - - self.value_on_not_set = value_on_not_set - - class DeadlineUrlEnumEntity(BaseEnumEntity): schema_types = ["deadline_url-enum"] diff --git a/openpype/settings/entities/schemas/system_schema/schema_modules.json b/openpype/settings/entities/schemas/system_schema/schema_modules.json index 31d8e04731..9961341ba5 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_modules.json +++ b/openpype/settings/entities/schemas/system_schema/schema_modules.json @@ -121,14 +121,7 @@ "collapsible_key": false, "object_type": { - "type": "dict", - "children": [ - { - "type": "providers-enum", - "key": "provider", - "label": "Provider" - } - ] + "type": "sync-server-providers" } } ] From 971844ed867e71a87f146a78f8be411bf6ba2a17 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 9 Sep 2021 16:25:20 +0200 Subject: [PATCH 41/62] nuke: python3 compatibility wip --- openpype/hosts/nuke/api/lib.py | 4 +++- openpype/hosts/nuke/plugins/publish/extract_ouput_node.py | 5 +++-- openpype/hosts/nuke/plugins/publish/extract_thumbnail.py | 5 +++++ openpype/hosts/nuke/plugins/publish/precollect_workfile.py | 1 - openpype/hosts/nuke/startup/write_to_read.py | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 7e7cd27f90..257bf8d64e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -727,7 +727,7 @@ class WorkfileSettings(object): log.error(msg) nuke.message(msg) - log.warning(">> root_dict: {}".format(root_dict)) + log.debug(">> root_dict: {}".format(root_dict)) # first set OCIO if self._root_node["colorManagement"].value() \ @@ -1277,6 +1277,7 @@ class ExporterReview: def clean_nodes(self): for node in self._temp_nodes: nuke.delete(node) + self._temp_nodes = [] self.log.info("Deleted nodes...") @@ -1301,6 +1302,7 @@ class ExporterReviewLut(ExporterReview): lut_style=None): # initialize parent class ExporterReview.__init__(self, klass, instance) + self._temp_nodes = [] # deal with now lut defined in viewer lut if hasattr(klass, "viewer_lut_raw"): diff --git a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py index a144761e5f..c3a6a3b167 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py +++ b/openpype/hosts/nuke/plugins/publish/extract_ouput_node.py @@ -2,6 +2,7 @@ import nuke import pyblish.api from avalon.nuke import maintained_selection + class CreateOutputNode(pyblish.api.ContextPlugin): """Adding output node for each ouput write node So when latly user will want to Load .nk as LifeGroup or Precomp @@ -15,8 +16,8 @@ class CreateOutputNode(pyblish.api.ContextPlugin): def process(self, context): # capture selection state with maintained_selection(): - active_node = [node for inst in context[:] - for node in inst[:] + active_node = [node for inst in context + for node in inst if "ak:family" in node.knobs()] if active_node: diff --git a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py index 55f7b746fc..0c9af66435 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/nuke/plugins/publish/extract_thumbnail.py @@ -1,3 +1,4 @@ +import sys import os import nuke from avalon.nuke import lib as anlib @@ -5,6 +6,10 @@ import pyblish.api import openpype +if sys.version_info[0] >= 3: + unicode = str + + class ExtractThumbnail(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts diff --git a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py index 5d3eb5f609..e10cfe7b47 100644 --- a/openpype/hosts/nuke/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/nuke/plugins/publish/precollect_workfile.py @@ -3,7 +3,6 @@ import pyblish.api import os import openpype.api as pype from avalon.nuke import lib as anlib -reload(anlib) class CollectWorkfile(pyblish.api.ContextPlugin): diff --git a/openpype/hosts/nuke/startup/write_to_read.py b/openpype/hosts/nuke/startup/write_to_read.py index deb5ce1b82..295a6e3c85 100644 --- a/openpype/hosts/nuke/startup/write_to_read.py +++ b/openpype/hosts/nuke/startup/write_to_read.py @@ -69,7 +69,8 @@ def evaluate_filepath_new(k_value, k_eval, project_dir, first_frame): frames = sorted(frames) firstframe = frames[0] lastframe = frames[len(frames) - 1] - if lastframe < 0: + + if int(lastframe) < 0: lastframe = firstframe return filepath, firstframe, lastframe From 42bb2a866c1a56131119eff6562e3c9590ef5f94 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Sep 2021 10:37:04 +0200 Subject: [PATCH 42/62] nuke: fixing issue with shared python object --- .../hosts/nuke/plugins/publish/extract_review_data_lut.py | 6 ++++++ .../hosts/nuke/plugins/publish/extract_review_data_mov.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py index b0d3ec6241..a0f1c9a087 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_lut.py @@ -3,6 +3,12 @@ import pyblish.api from avalon.nuke import lib as anlib from openpype.hosts.nuke.api import lib as pnlib import openpype + +try: + from __builtin__ import reload +except ImportError: + from importlib import reload + reload(pnlib) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index cea7d86c26..f4fbc2d0e4 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -4,6 +4,13 @@ from avalon.nuke import lib as anlib from openpype.hosts.nuke.api import lib as pnlib import openpype +try: + from __builtin__ import reload +except ImportError: + from importlib import reload + +reload(pnlib) + class ExtractReviewDataMov(openpype.api.Extractor): """Extracts movie and thumbnail with baked in luts From a16924d0c4a3845722eba9146c4379a58b4f58f7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Sep 2021 10:46:28 +0200 Subject: [PATCH 43/62] nuke: removing (Testing only) from nuke and nukex --- openpype/settings/defaults/system_settings/applications.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 842c294599..cfdeca4b87 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -195,7 +195,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", @@ -331,7 +331,7 @@ "environment": {} }, "__dynamic_keys_labels__": { - "13-0": "13.0 (Testing only)", + "13-0": "13.0", "12-2": "12.2", "12-0": "12.0", "11-3": "11.3", From a0338cb548b94fccd9ed0d1b799f10d80eb4a9d2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 13 Sep 2021 10:40:29 +0200 Subject: [PATCH 44/62] avalon-core update --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index f48fce09c0..b3e4959778 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit f48fce09c0986c1fd7f6731de33907be46b436c5 +Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6 From 0cb2cbb14f39a52befde9a6e7d7f4635594618fc Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 13 Sep 2021 12:27:31 +0200 Subject: [PATCH 45/62] Update openpype/modules/README.md --- openpype/modules/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/README.md b/openpype/modules/README.md index abc7ed3961..5716324365 100644 --- a/openpype/modules/README.md +++ b/openpype/modules/README.md @@ -9,7 +9,7 @@ OpenPype modules should contain separated logic of specific kind of implementati ### TODOs - add module/addon manifest - definition of module (not 100% defined content e.g. minimum required OpenPype version etc.) - - defying that folder is content of a module or an addon + - defining a folder as a content of a module or an addon ## Base class `OpenPypeModule` - abstract class as base for each module From 65b9bcf6e1c43746c9f7038db19602e21dcca925 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 09:45:07 +0200 Subject: [PATCH 46/62] removed arguments for storing credentials and event paths --- website/docs/admin_openpype_commands.md | 7 ++----- website/docs/module_ftrack.md | 5 ----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/website/docs/admin_openpype_commands.md b/website/docs/admin_openpype_commands.md index d6ccc883b0..7a46ee7906 100644 --- a/website/docs/admin_openpype_commands.md +++ b/website/docs/admin_openpype_commands.md @@ -55,7 +55,7 @@ openpype_console tray --debug --- ### `launch` arguments {#eventserver-arguments} You have to set either proper environment variables to provide URL and credentials or use -option to specify them. If you use `--store_credentials` provided credentials will be stored for later use. +option to specify them. | Argument | Description | | --- | --- | @@ -63,16 +63,13 @@ option to specify them. If you use `--store_credentials` provided credentials wi | `--ftrack-url` | URL to ftrack server (can be set with `FTRACK_SERVER`) | | `--ftrack-user` |user name to log in to ftrack (can be set with `FTRACK_API_USER`) | | `--ftrack-api-key` | ftrack api key (can be set with `FTRACK_API_KEY`) | -| `--ftrack-events-path` | path to event server plugins (can be set with `FTRACK_EVENTS_PATH`) | -| `--no-stored-credentials` | will use credential specified with options above | -| `--store-credentials` | will store credentials to file for later use | | `--legacy` | run event server without mongo storing | | `--clockify-api-key` | Clockify API key (can be set with `CLOCKIFY_API_KEY`) | | `--clockify-workspace` | Clockify workspace (can be set with `CLOCKIFY_WORKSPACE`) | To run ftrack event server: ```shell -openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= --ftrack-events-path= --no-stored-credentials --store-credentials +openpype_console eventserver --ftrack-url= --ftrack-user= --ftrack-api-key= ``` --- diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index 005270b3b9..cafee628c1 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -51,10 +51,7 @@ There are specific launch arguments for event server. With `openpype_console eve - **`--ftrack-user "your.username"`** : Ftrack Username - **`--ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee"`** : User's API key -- **`--store-crededentials`** : Entered credentials will be stored for next launch with this argument _(It is not needed to enter **ftrackuser** and **ftrackapikey** args on next launch)_ -- **`--no-stored-credentials`** : Stored credentials are loaded first so if you want to change credentials use this argument - `--ftrack-url "https://yourdomain.ftrackapp.com/"` : Ftrack server URL _(it is not needed to enter if you have set `FTRACK_SERVER` in OpenPype' environments)_ -- `--ftrack-events-path "//Paths/To/Events/"` : Paths to events folder. May contain multiple paths separated by `;`. _(it is not needed to enter if you have set `FTRACK_EVENTS_PATH` in OpenPype' environments)_ So if you want to use OpenPype's environments then you can launch event server for first time with these arguments `openpype_console.exe eventserver --ftrack-user "my.username" --ftrack-api-key "00000aaa-11bb-22cc-33dd-444444eeeee" --store-credentials`. Since that time, if everything was entered correctly, you can launch event server with `openpype_console.exe eventserver`. @@ -64,8 +61,6 @@ So if you want to use OpenPype's environments then you can launch event server f - `FTRACK_API_USER` - Username _("your.username")_ - `FTRACK_API_KEY` - User's API key _("00000aaa-11bb-22cc-33dd-444444eeeee")_ - `FTRACK_SERVER` - Ftrack server url _(")_ -- `FTRACK_EVENTS_PATH` - Paths to events _("//Paths/To/Events/")_ - We do not recommend you this way. From 2e5800cf8c2bb355175d189ec3468b42aeb835d7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 09:48:16 +0200 Subject: [PATCH 47/62] updated how to prepare shell scripts --- website/docs/module_ftrack.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/website/docs/module_ftrack.md b/website/docs/module_ftrack.md index cafee628c1..8e3806828d 100644 --- a/website/docs/module_ftrack.md +++ b/website/docs/module_ftrack.md @@ -98,10 +98,12 @@ Event server should **not** run more than once! It may cause major issues. `sudo vi /opt/openpype/run_event_server.sh` - add content to the file: ```sh -#!/usr/bin/env -export OPENPYPE_DEBUG=3 -pushd /mnt/pipeline/prod/openpype-setup -. openpype_console eventserver --ftrack-user --ftrack-api-key +#!/usr/bin/env bash +export OPENPYPE_DEBUG=1 +export OPENPYPE_MONGO= + +pushd /mnt/path/to/openpype +./openpype_console eventserver --ftrack-user --ftrack-api-key ``` - change file permission: `sudo chmod 0755 /opt/openpype/run_event_server.sh` @@ -141,9 +143,11 @@ WantedBy=multi-user.target - add content to the service file: ```sh @echo off -set OPENPYPE_DEBUG=3 -pushd \\path\to\file\ -openpype_console.exe eventserver --ftrack-user --ftrack-api-key +set OPENPYPE_DEBUG=1 +set OPENPYPE_MONGO= + +pushd \\path\to\openpype +openpype_console.exe eventserver --ftrack-user --ftrack-api-key ``` - download and install `nssm.cc` - create Windows service according to nssm.cc manual From e7046b09d7f7373bbe9e10931afde8dbe1646974 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:30:37 +0200 Subject: [PATCH 48/62] synchronization logic is encapsulated in `synchronization` method --- .../action_sync_to_avalon.py | 22 ++++++++++------- .../action_sync_to_avalon.py | 24 ++++++++++++------- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index d449c4b7df..aa5b95b207 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -52,17 +52,21 @@ class SyncToAvalonServer(ServerAction): return False def launch(self, session, in_entities, event): + project_entity = self.get_project_from_entity(in_entities[0]) + project_name = project_entity["full_name"] + result = self.synchronization( + session, in_entities, event, project_name + ) + + return result + + def synchronization(self, session, in_entities, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) - # Get ftrack project - if in_entities[0].entity_type.lower() == "project": - ft_project_name = in_entities[0]["full_name"] - else: - ft_project_name = in_entities[0]["project"]["full_name"] try: - output = self.entities_factory.launch_setup(ft_project_name) + output = self.entities_factory.launch_setup(project_name) if output is not None: return output @@ -72,7 +76,7 @@ class SyncToAvalonServer(ServerAction): time_2 = time.time() # This must happen before all filtering!!! - self.entities_factory.prepare_avalon_entities(ft_project_name) + self.entities_factory.prepare_avalon_entities(project_name) time_3 = time.time() self.entities_factory.filter_by_ignore_sync() @@ -118,7 +122,7 @@ class SyncToAvalonServer(ServerAction): report = self.entities_factory.report() if report and report.get("items"): default_title = "Synchronization report ({}):".format( - ft_project_name + project_name ) self.show_interface( items=report["items"], @@ -135,7 +139,6 @@ class SyncToAvalonServer(ServerAction): "Synchronization failed due to code error", exc_info=True ) msg = "An error has happened during synchronization" - title = "Synchronization report ({}):".format(ft_project_name) items = [] items.append({ "type": "label", @@ -160,6 +163,7 @@ class SyncToAvalonServer(ServerAction): report = self.entities_factory.report() except Exception: pass + title = "Synchronization report ({}):".format(project_name) _items = report.get("items", []) if _items: diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index d6ca561bbe..a57bb819a4 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -63,17 +63,23 @@ class SyncToAvalonLocal(BaseAction): return is_valid def launch(self, session, in_entities, event): + project_entity = self.get_project_from_entity(in_entities[0]) + project_name = project_entity["full_name"] + + result = self.synchronization( + session, in_entities, event, project_name + ) + + + return result + + def synchronization(self, session, in_entities, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) - # Get ftrack project - if in_entities[0].entity_type.lower() == "project": - ft_project_name = in_entities[0]["full_name"] - else: - ft_project_name = in_entities[0]["project"]["full_name"] try: - output = self.entities_factory.launch_setup(ft_project_name) + output = self.entities_factory.launch_setup(project_name) if output is not None: return output @@ -83,7 +89,7 @@ class SyncToAvalonLocal(BaseAction): time_2 = time.time() # This must happen before all filtering!!! - self.entities_factory.prepare_avalon_entities(ft_project_name) + self.entities_factory.prepare_avalon_entities(project_name) time_3 = time.time() self.entities_factory.filter_by_ignore_sync() @@ -129,7 +135,7 @@ class SyncToAvalonLocal(BaseAction): report = self.entities_factory.report() if report and report.get("items"): default_title = "Synchronization report ({}):".format( - ft_project_name + project_name ) self.show_interface( items=report["items"], @@ -146,7 +152,6 @@ class SyncToAvalonLocal(BaseAction): "Synchronization failed due to code error", exc_info=True ) msg = "An error occurred during synchronization" - title = "Synchronization report ({}):".format(ft_project_name) items = [] items.append({ "type": "label", @@ -181,6 +186,7 @@ class SyncToAvalonLocal(BaseAction): return {"success": True, "message": msg} + title = "Synchronization report ({}):".format(project_name) finally: try: self.entities_factory.dbcon.uninstall() From 2bd9ef0b53d1a95df767057cc2cf79c835b3dccf Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:35:10 +0200 Subject: [PATCH 49/62] exception handling moved from 'synchronization' to 'launch' --- .../action_sync_to_avalon.py | 75 ++++++++----------- .../action_sync_to_avalon.py | 75 ++++++++----------- 2 files changed, 61 insertions(+), 89 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index aa5b95b207..7f9074907a 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -54,13 +54,40 @@ class SyncToAvalonServer(ServerAction): def launch(self, session, in_entities, event): project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] - result = self.synchronization( - session, in_entities, event, project_name - ) + + try: + result = self.synchronization(event, project_name) + + except Exception as exc: + self.log.error( + "Synchronization failed due to code error", exc_info=True + ) + msg = "An error has happened during synchronization" + title = "Synchronization report ({}):".format(project_name) + items = [] + items.append({ + "type": "label", + "value": "# {}".format(msg) + }) + + report = {} + try: + report = self.entities_factory.report() + except Exception: + pass + + _items = report.get("items") or [] + if _items: + items.append(self.entities_factory.report_splitter) + items.extend(_items) + + self.show_interface(items, title, event, submit_btn_label="Ok") + + return {"success": True, "message": msg} return result - def synchronization(self, session, in_entities, event, project_name): + def synchronization(self, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) @@ -134,46 +161,6 @@ class SyncToAvalonServer(ServerAction): "message": "Synchronization Finished" } - except Exception: - self.log.error( - "Synchronization failed due to code error", exc_info=True - ) - msg = "An error has happened during synchronization" - items = [] - items.append({ - "type": "label", - "value": "# {}".format(msg) - }) - items.append({ - "type": "label", - "value": "## Traceback of the error" - }) - items.append({ - "type": "label", - "value": "

{}

".format( - str(traceback.format_exc()).replace( - "\n", "
").replace( - " ", " " - ) - ) - }) - - report = {"items": []} - try: - report = self.entities_factory.report() - except Exception: - pass - title = "Synchronization report ({}):".format(project_name) - - _items = report.get("items", []) - if _items: - items.append(self.entities_factory.report_splitter) - items.extend(_items) - - self.show_interface(items, title, event) - - return {"success": True, "message": msg} - finally: try: self.entities_factory.dbcon.uninstall() diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index a57bb819a4..4d030d03e8 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -65,15 +65,40 @@ class SyncToAvalonLocal(BaseAction): def launch(self, session, in_entities, event): project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] - - result = self.synchronization( - session, in_entities, event, project_name - ) + try: + result = self.synchronization(event, project_name) + + except Exception as exc: + self.log.error( + "Synchronization failed due to code error", exc_info=True + ) + msg = "An error has happened during synchronization" + title = "Synchronization report ({}):".format(project_name) + items = [] + items.append({ + "type": "label", + "value": "# {}".format(msg) + }) + + report = {} + try: + report = self.entities_factory.report() + except Exception: + pass + + _items = report.get("items") or [] + if _items: + items.append(self.entities_factory.report_splitter) + items.extend(_items) + + self.show_interface(items, title, event, submit_btn_label="Ok") + + return {"success": True, "message": msg} return result - def synchronization(self, session, in_entities, event, project_name): + def synchronization(self, event, project_name): time_start = time.time() self.show_message(event, "Synchronization - Preparing data", True) @@ -147,46 +172,6 @@ class SyncToAvalonLocal(BaseAction): "message": "Synchronization Finished" } - except Exception: - self.log.error( - "Synchronization failed due to code error", exc_info=True - ) - msg = "An error occurred during synchronization" - items = [] - items.append({ - "type": "label", - "value": "# {}".format(msg) - }) - items.append({ - "type": "label", - "value": "## Traceback of the error" - }) - items.append({ - "type": "label", - "value": "

{}

".format( - str(traceback.format_exc()).replace( - "\n", "
").replace( - " ", " " - ) - ) - }) - - report = {"items": []} - try: - report = self.entities_factory.report() - except Exception: - pass - - _items = report.get("items", []) - if _items: - items.append(self.entities_factory.report_splitter) - items.extend(_items) - - self.show_interface(items, title, event) - - return {"success": True, "message": msg} - - title = "Synchronization report ({}):".format(project_name) finally: try: self.entities_factory.dbcon.uninstall() From 71a2dd8a677f6daded58d02192e1ccc9791765ac Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:35:40 +0200 Subject: [PATCH 50/62] added job of synchronization where can be uploaded traceback --- .../action_sync_to_avalon.py | 34 +++++++++++++++++++ .../action_sync_to_avalon.py | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index 7f9074907a..9d3dee9e71 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -1,4 +1,6 @@ import time +import sys +import json import traceback from openpype_modules.ftrack.lib import ServerAction @@ -52,6 +54,20 @@ class SyncToAvalonServer(ServerAction): return False def launch(self, session, in_entities, event): + self.log.debug("{}: Creating job".format(self.label)) + + user_entity = session.query( + "User where id is {}".format(event["source"]["user"]["id"]) + ).one() + job_entity = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Sync to avalon is running..." + }) + }) + session.commit() + project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] @@ -62,6 +78,12 @@ class SyncToAvalonServer(ServerAction): self.log.error( "Synchronization failed due to code error", exc_info=True ) + + description = "Sync to avalon Crashed (Download traceback)" + self.add_traceback_to_job( + job_entity, session, sys.exc_info(), description + ) + msg = "An error has happened during synchronization" title = "Synchronization report ({}):".format(project_name) items = [] @@ -69,6 +91,12 @@ class SyncToAvalonServer(ServerAction): "type": "label", "value": "# {}".format(msg) }) + items.append({ + "type": "label", + "value": ( + "

Download report from job for more information.

" + ) + }) report = {} try: @@ -85,6 +113,12 @@ class SyncToAvalonServer(ServerAction): return {"success": True, "message": msg} + job_entity["status"] = "done" + job_entity["data"] = json.dumps({ + "description": "Sync to avalon finished." + }) + session.commit() + return result def synchronization(self, event, project_name): diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index 4d030d03e8..7d345771e8 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -1,4 +1,6 @@ import time +import sys +import json import traceback from openpype_modules.ftrack.lib import BaseAction, statics_icon @@ -63,6 +65,20 @@ class SyncToAvalonLocal(BaseAction): return is_valid def launch(self, session, in_entities, event): + self.log.debug("{}: Creating job".format(self.label)) + + user_entity = session.query( + "User where id is {}".format(event["source"]["user"]["id"]) + ).one() + job_entity = session.create("Job", { + "user": user_entity, + "status": "running", + "data": json.dumps({ + "description": "Sync to avalon is running..." + }) + }) + session.commit() + project_entity = self.get_project_from_entity(in_entities[0]) project_name = project_entity["full_name"] @@ -73,6 +89,12 @@ class SyncToAvalonLocal(BaseAction): self.log.error( "Synchronization failed due to code error", exc_info=True ) + + description = "Sync to avalon Crashed (Download traceback)" + self.add_traceback_to_job( + job_entity, session, sys.exc_info(), description + ) + msg = "An error has happened during synchronization" title = "Synchronization report ({}):".format(project_name) items = [] @@ -80,6 +102,12 @@ class SyncToAvalonLocal(BaseAction): "type": "label", "value": "# {}".format(msg) }) + items.append({ + "type": "label", + "value": ( + "

Download report from job for more information.

" + ) + }) report = {} try: @@ -96,6 +124,12 @@ class SyncToAvalonLocal(BaseAction): return {"success": True, "message": msg} + job_entity["status"] = "done" + job_entity["data"] = json.dumps({ + "description": "Sync to avalon finished." + }) + session.commit() + return result def synchronization(self, event, project_name): From c13f6132d624942b6c5b4a9f3e6fa95cd645cf23 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:36:07 +0200 Subject: [PATCH 51/62] removed irelevant comments --- .../ftrack/event_handlers_user/action_sync_to_avalon.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index 7d345771e8..2cb9ab4610 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -32,17 +32,10 @@ class SyncToAvalonLocal(BaseAction): - or do it manually (Not recommended) """ - #: Action identifier. identifier = "sync.to.avalon.local" - #: Action label. label = "OpenPype Admin" - #: Action variant variant = "- Sync To Avalon (Local)" - #: Action description. - description = "Send data from Ftrack to Avalon" - #: priority priority = 200 - #: roles that are allowed to register this action icon = statics_icon("ftrack", "action_icons", "OpenPypeAdmin.svg") settings_key = "sync_to_avalon_local" From 0749a96492696c67380b3e3dacd8470b4333497c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:36:29 +0200 Subject: [PATCH 52/62] `show_interface` can change submit button label --- .../ftrack/lib/ftrack_base_handler.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py b/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py index 7027154d86..a457b886ac 100644 --- a/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py +++ b/openpype/modules/default_modules/ftrack/lib/ftrack_base_handler.py @@ -384,8 +384,8 @@ class BaseHandler(object): ) def show_interface( - self, items, title='', - event=None, user=None, username=None, user_id=None + self, items, title="", event=None, user=None, + username=None, user_id=None, submit_btn_label=None ): """ Shows interface to user @@ -428,14 +428,18 @@ class BaseHandler(object): 'applicationId=ftrack.client.web and user.id="{0}"' ).format(user_id) + event_data = { + "type": "widget", + "items": items, + "title": title + } + if submit_btn_label: + event_data["submit_button_label"] = submit_btn_label + self.session.event_hub.publish( ftrack_api.event.base.Event( topic='ftrack.action.trigger-user-interface', - data=dict( - type='widget', - items=items, - title=title - ), + data=event_data, target=target ), on_error='ignore' @@ -443,7 +447,7 @@ class BaseHandler(object): def show_interface_from_dict( self, messages, title="", event=None, - user=None, username=None, user_id=None + user=None, username=None, user_id=None, submit_btn_label=None ): if not messages: self.log.debug("No messages to show! (messages dict is empty)") @@ -469,7 +473,9 @@ class BaseHandler(object): message = {'type': 'label', 'value': '

{}

'.format(value)} items.append(message) - self.show_interface(items, title, event, user, username, user_id) + self.show_interface( + items, title, event, user, username, user_id, submit_btn_label + ) def trigger_action( self, action_name, event=None, session=None, From 57af5bdf08b920971b19aead1f443c037f907118 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 10:41:54 +0200 Subject: [PATCH 53/62] removed unused exception variables --- .../ftrack/event_handlers_server/action_sync_to_avalon.py | 2 +- .../ftrack/event_handlers_user/action_sync_to_avalon.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py index 9d3dee9e71..58f79e8a2b 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_server/action_sync_to_avalon.py @@ -74,7 +74,7 @@ class SyncToAvalonServer(ServerAction): try: result = self.synchronization(event, project_name) - except Exception as exc: + except Exception: self.log.error( "Synchronization failed due to code error", exc_info=True ) diff --git a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py index 2cb9ab4610..cd2f371f38 100644 --- a/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py +++ b/openpype/modules/default_modules/ftrack/event_handlers_user/action_sync_to_avalon.py @@ -78,7 +78,7 @@ class SyncToAvalonLocal(BaseAction): try: result = self.synchronization(event, project_name) - except Exception as exc: + except Exception: self.log.error( "Synchronization failed due to code error", exc_info=True ) From 1b3771c3b3dca919a8895e899426a9f32683ae3c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 13:32:44 +0200 Subject: [PATCH 54/62] fix python 2 host breaking line --- openpype/lib/path_tools.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/path_tools.py b/openpype/lib/path_tools.py index 9dc14497a4..048bf0eda0 100644 --- a/openpype/lib/path_tools.py +++ b/openpype/lib/path_tools.py @@ -178,7 +178,9 @@ def _list_path_items(folder_structure): if not isinstance(path, (list, tuple)): path = [path] - output.append([key, *path]) + item = [key] + item.extend(path) + output.append(item) return output From b71898e1d8020dd2c136e08fe17b9c26dfc7722b Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:51:23 +0200 Subject: [PATCH 55/62] replaced 'get_pype_version' with 'get_openpype_version' --- openpype/lib/pype_info.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index c56782be9e..ec04f50532 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -11,11 +11,20 @@ from .execute import get_pype_execute_args from .local_settings import get_local_site_id -def get_pype_version(): +def get_openpype_version(): """Version of pype that is currently used.""" return openpype.version.__version__ +def get_pype_version(): + """Backwards compatibility. Remove when 100% not used.""" + print(( + "Using deprecated function 'openpype.lib.pype_info.get_pype_version'" + " replace with 'openpype.lib.pype_info.get_openpype_version'." + )) + return get_openpype_version() + + def get_pype_info(): """Information about currently used Pype process.""" executable_args = get_pype_execute_args() @@ -25,7 +34,7 @@ def get_pype_info(): version_type = "code" return { - "version": get_pype_version(), + "version": get_openpype_version(), "version_type": version_type, "executable": executable_args[-1], "pype_root": os.environ["OPENPYPE_REPOS_ROOT"], @@ -73,7 +82,7 @@ def extract_pype_info_to_file(dirpath): filepath (str): Full path to file where data were extracted. """ filename = "{}_{}_{}.json".format( - get_pype_version(), + get_openpype_version(), get_local_site_id(), datetime.datetime.now().strftime("%y%m%d%H%M%S") ) From 39233653f893b7ffc547d174dfb52bfd2f61cc9f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:56:09 +0200 Subject: [PATCH 56/62] added 'is_running_from_build' function --- openpype/lib/pype_info.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index ec04f50532..2feeab7cae 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -25,10 +25,23 @@ def get_pype_version(): return get_openpype_version() +def is_running_from_build(): + """Determine if current process is running from build or code. + + Returns: + bool: True if running from build. + """ + executable_path = os.environ["OPENPYPE_EXECUTABLE"] + executable_filename = os.path.basename(executable_path) + if "python" in executable_filename.lower(): + return False + return True + + def get_pype_info(): """Information about currently used Pype process.""" executable_args = get_pype_execute_args() - if len(executable_args) == 1: + if is_running_from_build(): version_type = "build" else: version_type = "code" From 2d826a65ca7c039f44e6b35eafbf2c331db66144 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 15:56:46 +0200 Subject: [PATCH 57/62] added 'get_build_version' to be able get build version --- openpype/lib/pype_info.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/openpype/lib/pype_info.py b/openpype/lib/pype_info.py index 2feeab7cae..c50c4db94b 100644 --- a/openpype/lib/pype_info.py +++ b/openpype/lib/pype_info.py @@ -9,6 +9,7 @@ import openpype.version from openpype.settings.lib import get_local_settings from .execute import get_pype_execute_args from .local_settings import get_local_site_id +from .python_module_tools import import_filepath def get_openpype_version(): @@ -25,6 +26,25 @@ def get_pype_version(): return get_openpype_version() +def get_build_version(): + """OpenPype version of build.""" + # Return OpenPype version if is running from code + if not is_running_from_build(): + return get_openpype_version() + + # Import `version.py` from build directory + version_filepath = os.path.join( + os.environ["OPENPYPE_ROOT"], + "openpype", + "version.py" + ) + if not os.path.exists(version_filepath): + return None + + module = import_filepath(version_filepath, "openpype_build_version") + return getattr(module, "__version__", None) + + def is_running_from_build(): """Determine if current process is running from build or code. From 12b362a15554d65b405509644fe29ec1eb9670a2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:02:13 +0200 Subject: [PATCH 58/62] added 'get_openpype_version' and 'get_build_version' to `openpype.lib` scope --- openpype/lib/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 9bc68c9558..e96f1cc99f 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -160,6 +160,11 @@ from .editorial import ( make_sequence_collection ) +from .pype_info import ( + get_openpype_version, + get_build_version +) + terminal = Terminal __all__ = [ @@ -280,5 +285,8 @@ __all__ = [ "frames_to_timecode", "make_sequence_collection", "create_project_folders", - "get_project_basic_paths" + "get_project_basic_paths", + + "get_openpype_version", + "get_build_version", ] From 9e7a44527520fe6300687f693a831853f7e00100 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:19:30 +0200 Subject: [PATCH 59/62] Statuser converts status data to OrderedDict --- .../default_modules/ftrack/scripts/sub_event_status.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py index 8a2733b635..004f61338c 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_status.py @@ -2,6 +2,7 @@ import os import sys import json import threading +import collections import signal import socket import datetime @@ -165,7 +166,7 @@ class StatusFactory: return source = event["data"]["source"] - data = event["data"]["status_info"] + data = collections.OrderedDict(event["data"]["status_info"]) self.update_status_info(source, data) @@ -348,7 +349,7 @@ def heartbeat(): def main(args): port = int(args[-1]) - server_info = json.loads(args[-2]) + server_info = collections.OrderedDict(json.loads(args[-2])) # Create a TCP/IP socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) From ce379e3cb42f4a191e35bd6a82c1b1a08fe4465e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:26:05 +0200 Subject: [PATCH 60/62] add openpype version information about each process --- .../ftrack/ftrack_server/event_server_cli.py | 19 +++++++++++-------- .../ftrack/scripts/sub_event_processor.py | 13 ++++++++++--- .../ftrack/scripts/sub_event_storer.py | 14 ++++++++++---- 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index d8e4d05580..1eeda1fefd 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -6,7 +6,6 @@ import subprocess import socket import json import platform -import argparse import getpass import atexit import time @@ -16,7 +15,9 @@ import ftrack_api import pymongo from openpype.lib import ( get_pype_execute_args, - OpenPypeMongoConnection + OpenPypeMongoConnection, + get_openpype_version, + get_build_version ) from openpype_modules.ftrack import FTRACK_MODULE_DIR from openpype_modules.ftrack.lib import credentials @@ -238,12 +239,14 @@ def main_loop(ftrack_url): system_name, pc_name = platform.uname()[:2] host_name = socket.gethostname() - main_info = { - "created_at": datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S"), - "Username": getpass.getuser(), - "Host Name": host_name, - "Host IP": socket.gethostbyname(host_name) - } + main_info = [ + ["created_at", datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")], + ["Username", getpass.getuser()], + ["Host Name", host_name], + ["Host IP", socket.gethostbyname(host_name)], + ["OpenPype version", get_openpype_version() or "N/A"], + ["OpenPype build version", get_build_version() or "N/A"] + ] main_info_str = json.dumps(main_info) # Main loop while True: diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py index 51b45eb93b..d1e2e3aaeb 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_processor.py @@ -13,6 +13,11 @@ from openpype_modules.ftrack.ftrack_server.lib import ( from openpype.modules import ModulesManager from openpype.api import Logger +from openpype.lib import ( + get_openpype_version, + get_build_version +) + import ftrack_api @@ -40,9 +45,11 @@ def send_status(event): new_event_data = { "subprocess_id": subprocess_id, "source": "processor", - "status_info": { - "created_at": subprocess_started.strftime("%Y.%m.%d %H:%M:%S") - } + "status_info": [ + ["created_at", subprocess_started.strftime("%Y.%m.%d %H:%M:%S")], + ["OpenPype version", get_openpype_version() or "N/A"], + ["OpenPype build version", get_build_version() or "N/A"] + ] } new_event = ftrack_api.event.base.Event( diff --git a/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py b/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py index a8649e0ccc..5543ed74e2 100644 --- a/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py +++ b/openpype/modules/default_modules/ftrack/scripts/sub_event_storer.py @@ -14,7 +14,11 @@ from openpype_modules.ftrack.ftrack_server.lib import ( TOPIC_STATUS_SERVER_RESULT ) from openpype_modules.ftrack.lib import get_ftrack_event_mongo_info -from openpype.lib import OpenPypeMongoConnection +from openpype.lib import ( + OpenPypeMongoConnection, + get_openpype_version, + get_build_version +) from openpype.api import Logger log = Logger.get_logger("Event storer") @@ -153,9 +157,11 @@ def send_status(event): new_event_data = { "subprocess_id": os.environ["FTRACK_EVENT_SUB_ID"], "source": "storer", - "status_info": { - "created_at": subprocess_started.strftime("%Y.%m.%d %H:%M:%S") - } + "status_info": [ + ["created_at", subprocess_started.strftime("%Y.%m.%d %H:%M:%S")], + ["OpenPype version", get_openpype_version() or "N/A"], + ["OpenPype build version", get_build_version() or "N/A"] + ] } new_event = ftrack_api.event.base.Event( From 01a3e96b72e1861720be3c4f1cb0099ebaf616cc Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Sep 2021 16:26:20 +0200 Subject: [PATCH 61/62] added openpype executable to main process info --- .../default_modules/ftrack/ftrack_server/event_server_cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py index 1eeda1fefd..075694d8f6 100644 --- a/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py +++ b/openpype/modules/default_modules/ftrack/ftrack_server/event_server_cli.py @@ -237,13 +237,13 @@ def main_loop(ftrack_url): statuser_thread=statuser_thread ) - system_name, pc_name = platform.uname()[:2] host_name = socket.gethostname() main_info = [ ["created_at", datetime.datetime.now().strftime("%Y.%m.%d %H:%M:%S")], ["Username", getpass.getuser()], ["Host Name", host_name], ["Host IP", socket.gethostbyname(host_name)], + ["OpenPype executable", get_pype_execute_args()[-1]], ["OpenPype version", get_openpype_version() or "N/A"], ["OpenPype build version", get_build_version() or "N/A"] ] From 9a8fa8311e5843bfefb702887c05490f83a72d74 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 15 Sep 2021 13:28:17 +0200 Subject: [PATCH 62/62] update avalon core submodule --- repos/avalon-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/repos/avalon-core b/repos/avalon-core index b3e4959778..1e94241ffe 160000 --- a/repos/avalon-core +++ b/repos/avalon-core @@ -1 +1 @@ -Subproject commit b3e49597786c931c13bca207769727d5fc56d5f6 +Subproject commit 1e94241ffe2dd7ce65ca66b08e452ffc03180235