From 46548eafa074350ce97eecc9e297e36878acddad Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 13:58:06 +0200 Subject: [PATCH 01/50] toggle setData method happens on item not on model --- pype/tools/pyblish_pype/window.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 21ac500f9c..8f8c65e6ce 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -415,9 +415,9 @@ class Window(QtWidgets.QDialog): QtCore.Qt.DirectConnection ) - artist_view.toggled.connect(self.on_item_toggled) - overview_instance_view.toggled.connect(self.on_item_toggled) - overview_plugin_view.toggled.connect(self.on_item_toggled) + artist_view.toggled.connect(self.on_instance_toggle) + overview_instance_view.toggled.connect(self.on_instance_toggle) + overview_plugin_view.toggled.connect(self.on_plugin_toggle) footer_button_stop.clicked.connect(self.on_stop_clicked) footer_button_reset.clicked.connect(self.on_reset_clicked) @@ -537,7 +537,7 @@ class Window(QtWidgets.QDialog): ): instance_item.setData(enable_value, Roles.IsEnabledRole) - def on_item_toggled(self, index, state=None): + def on_instance_toggle(self, index, state=None): """An item is requesting to be toggled""" if not index.data(Roles.IsOptionalRole): return self.info("This item is mandatory") @@ -548,7 +548,27 @@ class Window(QtWidgets.QDialog): if state is None: state = not index.data(QtCore.Qt.CheckStateRole) - index.model().setData(index, state, QtCore.Qt.CheckStateRole) + instance_id = index.data(Roles.ObjectIdRole) + instanceitem = self.instance_model.instance_items[instance_id] + instanceitem.setData(state, QtCore.Qt.CheckStateRole) + + self.update_compatibility() + + def on_plugin_toggle(self, index, state=None): + """An item is requesting to be toggled""" + if not index.data(Roles.IsOptionalRole): + return self.info("This item is mandatory") + + if self.controller.collect_state != 1: + return self.info("Cannot toggle") + + if state is None: + state = not index.data(QtCore.Qt.CheckStateRole) + + plugin_id = index.data(Roles.ObjectIdRole) + plugin_item = self.plugin_model.plugin_items[plugin_id] + plugin_item.setData(state, QtCore.Qt.CheckStateRole) + self.update_compatibility() def on_tab_changed(self, target): From c69bf58d3bdc85a8b03a121419f0105f3d107412 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jun 2020 12:59:42 +0100 Subject: [PATCH 02/50] Fix publishing workfile - Unified template export across workfile and templates. - Fix scene saving per instance. - Cleanup template loader. --- pype/hosts/harmony/__init__.py | 45 +++++++++++++++ .../harmony/load/load_template_workfile.py | 25 --------- .../harmony/publish/extract_save_scene.py | 2 +- .../harmony/publish/extract_template.py | 55 +++---------------- .../harmony/publish/extract_workfile.py | 20 +++++-- 5 files changed, 70 insertions(+), 77 deletions(-) diff --git a/pype/hosts/harmony/__init__.py b/pype/hosts/harmony/__init__.py index cdb8b40777..4105759527 100644 --- a/pype/hosts/harmony/__init__.py +++ b/pype/hosts/harmony/__init__.py @@ -84,6 +84,51 @@ def ensure_scene_settings(): harmony.send({"function": func, "args": [valid_settings]}) +def export_template(backdrops, nodes, filepath): + func = """function func(args) + { + // Add an extra node just so a new group can be created. + var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); + var template_group = node.createGroup(temp_node, "temp_group"); + node.deleteNode( template_group + "/temp_note" ); + + // This will make Node View to focus on the new group. + selection.clearSelection(); + selection.addNodeToSelection(template_group); + Action.perform("onActionEnterGroup()", "Node View"); + + // Recreate backdrops in group. + for (var i = 0 ; i < args[0].length; i++) + { + Backdrop.addBackdrop(template_group, args[0][i]); + }; + + // Copy-paste the selected nodes into the new group. + var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); + copyPaste.pasteNewNodes(drag_object, template_group, ""); + + // Select all nodes within group and export as template. + Action.perform( "selectAll()", "Node View" ); + copyPaste.createTemplateFromSelection(args[2], args[3]); + + // Unfocus the group in Node view, delete all nodes and backdrops + // created during the process. + Action.perform("onActionUpToParent()", "Node View"); + node.deleteNode(template_group, true, true); + } + func + """ + harmony.send({ + "function": func, + "args": [ + backdrops, + nodes, + os.path.basename(filepath), + os.path.dirname(filepath) + ] + }) + + def install(): print("Installing Pype config...") diff --git a/pype/plugins/harmony/load/load_template_workfile.py b/pype/plugins/harmony/load/load_template_workfile.py index 00d2e63c62..a9dcd0c776 100644 --- a/pype/plugins/harmony/load/load_template_workfile.py +++ b/pype/plugins/harmony/load/load_template_workfile.py @@ -14,18 +14,6 @@ class ImportTemplateLoader(api.Loader): label = "Import Template" def load(self, context, name=None, namespace=None, data=None): - # Make backdrops from metadata. - backdrops = context["representation"]["data"].get("backdrops", []) - - func = """function func(args) - { - Backdrop.addBackdrop("Top", args[0]); - } - func - """ - for backdrop in backdrops: - harmony.send({"function": func, "args": [backdrop]}) - # Import template. temp_dir = tempfile.mkdtemp() zip_file = api.get_representation_path(context["representation"]) @@ -33,19 +21,6 @@ class ImportTemplateLoader(api.Loader): with zipfile.ZipFile(zip_file, "r") as zip_ref: zip_ref.extractall(template_path) - func = """function func(args) - { - var template_path = args[0]; - var drag_object = copyPaste.copyFromTemplate( - template_path, 0, 0, copyPaste.getCurrentCreateOptions() - ); - copyPaste.pasteNewNodes( - drag_object, "", copyPaste.getCurrentPasteOptions() - ); - } - func - """ - func = """function func(args) { var template_path = args[0]; diff --git a/pype/plugins/harmony/publish/extract_save_scene.py b/pype/plugins/harmony/publish/extract_save_scene.py index 1733bdb95c..8b953580a7 100644 --- a/pype/plugins/harmony/publish/extract_save_scene.py +++ b/pype/plugins/harmony/publish/extract_save_scene.py @@ -9,5 +9,5 @@ class ExtractSaveScene(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.49 hosts = ["harmony"] - def process(self, instance): + def process(self, context): harmony.save_scene() diff --git a/pype/plugins/harmony/publish/extract_template.py b/pype/plugins/harmony/publish/extract_template.py index f7a5e34e67..1ba0befc54 100644 --- a/pype/plugins/harmony/publish/extract_template.py +++ b/pype/plugins/harmony/publish/extract_template.py @@ -2,7 +2,8 @@ import os import shutil import pype.api -from avalon import harmony +import avalon.harmony +import pype.hosts.harmony class ExtractTemplate(pype.api.Extractor): @@ -14,6 +15,7 @@ class ExtractTemplate(pype.api.Extractor): def process(self, instance): staging_dir = self.staging_dir(instance) + filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name)) self.log.info("Outputting template to {}".format(staging_dir)) @@ -28,7 +30,7 @@ class ExtractTemplate(pype.api.Extractor): unique_backdrops = [backdrops[x] for x in set(backdrops.keys())] # Get non-connected nodes within backdrops. - all_nodes = harmony.send( + all_nodes = avalon.harmony.send( {"function": "node.subNodes", "args": ["Top"]} )["result"] for node in [x for x in all_nodes if x not in dependencies]: @@ -43,48 +45,9 @@ class ExtractTemplate(pype.api.Extractor): dependencies.remove(instance[0]) # Export template. - func = """function func(args) - { - // Add an extra node just so a new group can be created. - var temp_node = node.add("Top", "temp_note", "NOTE", 0, 0, 0); - var template_group = node.createGroup(temp_node, "temp_group"); - node.deleteNode( template_group + "/temp_note" ); - - // This will make Node View to focus on the new group. - selection.clearSelection(); - selection.addNodeToSelection(template_group); - Action.perform("onActionEnterGroup()", "Node View"); - - // Recreate backdrops in group. - for (var i = 0 ; i < args[0].length; i++) - { - Backdrop.addBackdrop(template_group, args[0][i]); - }; - - // Copy-paste the selected nodes into the new group. - var drag_object = copyPaste.copy(args[1], 1, frame.numberOf, ""); - copyPaste.pasteNewNodes(drag_object, template_group, ""); - - // Select all nodes within group and export as template. - Action.perform( "selectAll()", "Node View" ); - copyPaste.createTemplateFromSelection(args[2], args[3]); - - // Unfocus the group in Node view, delete all nodes and backdrops - // created during the process. - Action.perform("onActionUpToParent()", "Node View"); - node.deleteNode(template_group, true, true); - } - func - """ - harmony.send({ - "function": func, - "args": [ - unique_backdrops, - dependencies, - "{}.tpl".format(instance.name), - staging_dir - ] - }) + pype.hosts.harmony.export_template( + unique_backdrops, dependencies, filepath + ) # Prep representation. os.chdir(staging_dir) @@ -131,7 +94,7 @@ class ExtractTemplate(pype.api.Extractor): } func """ - return harmony.send( + return avalon.harmony.send( {"function": func, "args": [node]} )["result"] @@ -150,7 +113,7 @@ class ExtractTemplate(pype.api.Extractor): func """ - current_dependencies = harmony.send( + current_dependencies = avalon.harmony.send( {"function": func, "args": [node]} )["result"] diff --git a/pype/plugins/harmony/publish/extract_workfile.py b/pype/plugins/harmony/publish/extract_workfile.py index 7a0a7954dd..304b70e293 100644 --- a/pype/plugins/harmony/publish/extract_workfile.py +++ b/pype/plugins/harmony/publish/extract_workfile.py @@ -2,6 +2,8 @@ import os import shutil import pype.api +import avalon.harmony +import pype.hosts.harmony class ExtractWorkfile(pype.api.Extractor): @@ -12,17 +14,25 @@ class ExtractWorkfile(pype.api.Extractor): families = ["workfile"] def process(self, instance): - file_path = instance.context.data["currentFile"] + # Export template. + backdrops = avalon.harmony.send( + {"function": "Backdrop.backdrops", "args": ["Top"]} + )["result"] + nodes = avalon.harmony.send( + {"function": "node.subNodes", "args": ["Top"]} + )["result"] staging_dir = self.staging_dir(instance) + filepath = os.path.join(staging_dir, "{}.tpl".format(instance.name)) + pype.hosts.harmony.export_template(backdrops, nodes, filepath) + + # Prep representation. os.chdir(staging_dir) shutil.make_archive( - instance.name, + "{}".format(instance.name), "zip", - os.path.dirname(file_path) + os.path.join(staging_dir, "{}.tpl".format(instance.name)) ) - zip_path = os.path.join(staging_dir, instance.name + ".zip") - self.log.info(f"Output zip file: {zip_path}") representation = { "name": "tpl", From b210e16c90acbdbf005c953c51bd0f0f33a8565a Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 16:55:25 +0200 Subject: [PATCH 03/50] widgets for log details are not set automatically for each log --- pype/tools/pyblish_pype/model.py | 2 -- pype/tools/pyblish_pype/widgets.py | 5 ----- pype/tools/pyblish_pype/window.py | 5 ----- 3 files changed, 12 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 2c2661b5ec..91c6c595eb 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -1053,7 +1053,6 @@ class TerminalModel(QtGui.QStandardItemModel): self.reset() def reset(self): - self.items_to_set_widget = queue.Queue() self.clear() def prepare_records(self, result): @@ -1144,7 +1143,6 @@ class TerminalModel(QtGui.QStandardItemModel): detail_item = QtGui.QStandardItem(detail_text) detail_item.setData(TerminalDetailType, Roles.TypeRole) top_item.appendRow(detail_item) - self.items_to_set_widget.put(detail_item) def update_with_result(self, result): for record in result["records"]: diff --git a/pype/tools/pyblish_pype/widgets.py b/pype/tools/pyblish_pype/widgets.py index e81633f7a3..880d4755ad 100644 --- a/pype/tools/pyblish_pype/widgets.py +++ b/pype/tools/pyblish_pype/widgets.py @@ -321,11 +321,6 @@ class PerspectiveWidget(QtWidgets.QWidget): data = {"records": records} self.terminal_model.reset() self.terminal_model.update_with_result(data) - while not self.terminal_model.items_to_set_widget.empty(): - item = self.terminal_model.items_to_set_widget.get() - widget = TerminalDetail(item.data(QtCore.Qt.DisplayRole)) - index = self.terminal_proxy.mapFromSource(item.index()) - self.terminal_view.setIndexWidget(index, widget) self.records.button_toggle_text.setText( "{} ({})".format(self.l_rec, len_records) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 21ac500f9c..3b70caa7c5 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -815,11 +815,6 @@ class Window(QtWidgets.QDialog): instance_item = self.instance_model.update_with_result(result) self.terminal_model.update_with_result(result) - while not self.terminal_model.items_to_set_widget.empty(): - item = self.terminal_model.items_to_set_widget.get() - widget = widgets.TerminalDetail(item.data(QtCore.Qt.DisplayRole)) - index = self.terminal_proxy.mapFromSource(item.index()) - self.terminal_view.setIndexWidget(index, widget) self.update_compatibility() From bc20ac38fd91ff578fdb214e0e6b541320a9fc71 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 16:55:54 +0200 Subject: [PATCH 04/50] widget for log detail is set before expanding --- pype/tools/pyblish_pype/view.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index ada19bc7d9..32d9bcb04d 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,5 +1,6 @@ from Qt import QtCore, QtWidgets from . import model +from . import widgets from .constants import Roles @@ -190,6 +191,20 @@ class TerminalView(QtWidgets.QTreeView): self.updateGeometry() self.scrollToBottom() + def expand(self, index): + """Wrapper to set widget for expanded index.""" + model = index.model() + row_count = model.rowCount(index) + for child_idx in range(row_count): + child_index = model.index(child_idx, index.column(), index) + widget = self.indexWidget(child_index) + if widget is None: + widget = widgets.TerminalDetail( + child_index.data(QtCore.Qt.DisplayRole) + ) + self.setIndexWidget(child_index, widget) + super(TerminalView, self).expand(index) + def resizeEvent(self, event): super(self.__class__, self).resizeEvent(event) self.model().layoutChanged.emit() From 086059cbf3519167cc165a099899d64eb72d689e Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 17:47:03 +0200 Subject: [PATCH 05/50] controller can handle instance toggle callbacks --- pype/tools/pyblish_pype/control.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index c454e4c6fa..21cf660e63 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -8,6 +8,7 @@ an active window manager; such as via Travis-CI. import os import sys import traceback +import inspect from Qt import QtCore @@ -60,11 +61,15 @@ class Controller(QtCore.QObject): # store OrderGroups - now it is a singleton order_groups = util.OrderGroups + # When instance is toggled + instance_toggled = QtCore.Signal(object, object, object) + def __init__(self, parent=None): super(Controller, self).__init__(parent) self.context = None self.plugins = {} self.optional_default = {} + self.instance_toggled.connect(self._on_instance_toggled) def reset_variables(self): # Data internal to the GUI itself @@ -410,3 +415,19 @@ class Controller(QtCore.QObject): for plugin in self.plugins: del(plugin) + + def _on_instance_toggled(self, instance, new_value, old_value): + callbacks = pyblish.api.registered_callbacks().get("instanceToggled") + if not callbacks: + return + + for callback in callbacks: + try: + callback(instance, new_value, old_value) + except Exception: + print( + "Callback for `instanceToggled` crashed. {}".format( + os.path.abspath(inspect.getfile(callback)) + ) + ) + traceback.print_exception(*sys.exc_info()) From 805ca009913bed2e163b365d597e0cf5171141e0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 17:47:22 +0200 Subject: [PATCH 06/50] pyblish gui trigger callbacks on instance toggle --- pype/tools/pyblish_pype/window.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 8f8c65e6ce..0c914a0c4b 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -545,12 +545,17 @@ class Window(QtWidgets.QDialog): if self.controller.collect_state != 1: return self.info("Cannot toggle") + current_state = index.data(QtCore.Qt.CheckStateRole) if state is None: - state = not index.data(QtCore.Qt.CheckStateRole) + state = not current_state instance_id = index.data(Roles.ObjectIdRole) - instanceitem = self.instance_model.instance_items[instance_id] - instanceitem.setData(state, QtCore.Qt.CheckStateRole) + instance_item = self.instance_model.instance_items[instance_id] + instance_item.setData(state, QtCore.Qt.CheckStateRole) + + self.controller.instance_toggled.emit( + instance_item.instance, state, current_state + ) self.update_compatibility() From e547967e1be58091aedc3fff4d4a6f3d1709f205 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 16 Jun 2020 18:47:57 +0200 Subject: [PATCH 07/50] reversed instance_toggled new/old value --- pype/tools/pyblish_pype/control.py | 4 ++-- pype/tools/pyblish_pype/window.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index 21cf660e63..aa39663182 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -416,14 +416,14 @@ class Controller(QtCore.QObject): for plugin in self.plugins: del(plugin) - def _on_instance_toggled(self, instance, new_value, old_value): + def _on_instance_toggled(self, instance, old_value, new_value): callbacks = pyblish.api.registered_callbacks().get("instanceToggled") if not callbacks: return for callback in callbacks: try: - callback(instance, new_value, old_value) + callback(instance, old_value, new_value) except Exception: print( "Callback for `instanceToggled` crashed. {}".format( diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 0c914a0c4b..5d22e5ac8f 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -554,7 +554,7 @@ class Window(QtWidgets.QDialog): instance_item.setData(state, QtCore.Qt.CheckStateRole) self.controller.instance_toggled.emit( - instance_item.instance, state, current_state + instance_item.instance, current_state, state ) self.update_compatibility() From 75366a3a3b2e5c6dd458f5517b13a6c197cdd813 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 11:07:12 +0200 Subject: [PATCH 08/50] log detail is calculated after expanding --- pype/tools/pyblish_pype/model.py | 90 +++++++++++++++++++------------- pype/tools/pyblish_pype/view.py | 9 ++-- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 91c6c595eb..6b8aaeb79e 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -32,7 +32,6 @@ from .awesome import tags as awesome import Qt from Qt import QtCore, QtGui from six import text_type -from six.moves import queue from .vendor import qtawesome from .constants import PluginStates, InstanceStates, GroupStates, Roles @@ -1009,7 +1008,7 @@ class ArtistProxy(QtCore.QAbstractProxyModel): return QtCore.QModelIndex() -class TerminalModel(QtGui.QStandardItemModel): +class TerminalDetailItem(QtGui.QStandardItem): key_label_record_map = ( ("instance", "Instance"), ("msg", "Message"), @@ -1022,6 +1021,57 @@ class TerminalModel(QtGui.QStandardItemModel): ("msecs", "Millis") ) + def __init__(self, record_item): + self.record_item = record_item + self.msg = None + msg = record_item.get("msg") + if msg is None: + msg = record_item["label"].split("\n")[0] + + super(TerminalDetailItem, self).__init__(msg) + + def data(self, role=QtCore.Qt.DisplayRole): + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if self.msg is None: + self.msg = self.compute_detail_text(self.record_item) + return self.msg + return super(TerminalDetailItem, self).data(role) + + def compute_detail_text(self, item_data): + if item_data["type"] == "info": + return item_data["label"] + + html_text = "" + for key, title in self.key_label_record_map: + if key not in item_data: + continue + value = item_data[key] + text = ( + str(value) + .replace("<", "<") + .replace(">", ">") + .replace('\n', '
') + .replace(' ', ' ') + ) + + title_tag = ( + '{}: ' + ' color:#fff;\" >{}: ' + ).format(title) + + html_text += ( + '{}' + '{}' + ).format(title_tag, text) + + html_text = '{}
'.format( + html_text + ) + return html_text + + +class TerminalModel(QtGui.QStandardItemModel): item_icon_name = { "info": "fa.info", "record": "fa.circle", @@ -1139,8 +1189,7 @@ class TerminalModel(QtGui.QStandardItemModel): self.appendRow(top_item) - detail_text = self.prepare_detail_text(record_item) - detail_item = QtGui.QStandardItem(detail_text) + detail_item = TerminalDetailItem(record_item) detail_item.setData(TerminalDetailType, Roles.TypeRole) top_item.appendRow(detail_item) @@ -1148,39 +1197,6 @@ class TerminalModel(QtGui.QStandardItemModel): for record in result["records"]: self.append(record) - def prepare_detail_text(self, item_data): - if item_data["type"] == "info": - return item_data["label"] - - html_text = "" - for key, title in self.key_label_record_map: - if key not in item_data: - continue - value = item_data[key] - text = ( - str(value) - .replace("<", "<") - .replace(">", ">") - .replace('\n', '
') - .replace(' ', ' ') - ) - - title_tag = ( - '{}: ' - ' color:#fff;\" >{}: ' - ).format(title) - - html_text += ( - '{}' - '{}' - ).format(title_tag, text) - - html_text = '{}
'.format( - html_text - ) - return html_text - class TerminalProxy(QtCore.QSortFilterProxyModel): filter_buttons_checks = { diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 32d9bcb04d..03509604bb 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -195,15 +195,18 @@ class TerminalView(QtWidgets.QTreeView): """Wrapper to set widget for expanded index.""" model = index.model() row_count = model.rowCount(index) + is_new = False for child_idx in range(row_count): child_index = model.index(child_idx, index.column(), index) widget = self.indexWidget(child_index) if widget is None: - widget = widgets.TerminalDetail( - child_index.data(QtCore.Qt.DisplayRole) - ) + is_new = True + msg = child_index.data(QtCore.Qt.DisplayRole) + widget = widgets.TerminalDetail(msg) self.setIndexWidget(child_index, widget) super(TerminalView, self).expand(index) + if is_new: + self.updateGeometries() def resizeEvent(self, event): super(self.__class__, self).resizeEvent(event) From b1bfe8a39d655b2c6a9c3092e9e9eb38a2d1b0ae Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 11:16:07 +0200 Subject: [PATCH 09/50] formatting cleanup --- pype/tools/pyblish_pype/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index 6b8aaeb79e..e316d5781a 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -48,6 +48,7 @@ TerminalDetailType = QtGui.QStandardItem.UserType + 4 class QAwesomeTextIconFactory: icons = {} + @classmethod def icon(cls, icon_name): if icon_name not in cls.icons: @@ -57,6 +58,7 @@ class QAwesomeTextIconFactory: class QAwesomeIconFactory: icons = {} + @classmethod def icon(cls, icon_name, icon_color): if icon_name not in cls.icons: From 13144b18cf4f286c2b20b6c0b871fdfcbb3e5e84 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 10:25:46 +0100 Subject: [PATCH 10/50] Optimize processing Ffmpeg validator is now a context plugin instead of instance. --- pype/plugins/global/publish/validate_ffmpeg_installed.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pype/plugins/global/publish/validate_ffmpeg_installed.py b/pype/plugins/global/publish/validate_ffmpeg_installed.py index f6738e6de1..61127ff96e 100644 --- a/pype/plugins/global/publish/validate_ffmpeg_installed.py +++ b/pype/plugins/global/publish/validate_ffmpeg_installed.py @@ -8,12 +8,11 @@ except ImportError: import errno -class ValidateFFmpegInstalled(pyblish.api.Validator): +class ValidateFFmpegInstalled(pyblish.api.ContextPlugin): """Validate availability of ffmpeg tool in PATH""" order = pyblish.api.ValidatorOrder label = 'Validate ffmpeg installation' - families = ['review'] optional = True def is_tool(self, name): @@ -27,7 +26,7 @@ class ValidateFFmpegInstalled(pyblish.api.Validator): return False return True - def process(self, instance): + def process(self, context): ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") self.log.info("ffmpeg path: `{}`".format(ffmpeg_path)) if self.is_tool(ffmpeg_path) is False: From 8f722095b078b3baa53f802bb102187897f9a414 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 10:28:27 +0100 Subject: [PATCH 11/50] Validate shots For duplicate shot names. --- .../publish/validate_shots.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pype/plugins/standalonepublisher/publish/validate_shots.py diff --git a/pype/plugins/standalonepublisher/publish/validate_shots.py b/pype/plugins/standalonepublisher/publish/validate_shots.py new file mode 100644 index 0000000000..3267af7685 --- /dev/null +++ b/pype/plugins/standalonepublisher/publish/validate_shots.py @@ -0,0 +1,23 @@ +import pyblish.api +import pype.api + + +class ValidateShots(pyblish.api.ContextPlugin): + """Validate there is a "mov" next to the editorial file.""" + + label = "Validate Shots" + hosts = ["standalonepublisher"] + order = pype.api.ValidateContentsOrder + + def process(self, context): + shot_names = [] + duplicate_names = [] + for instance in context: + name = instance.data["name"] + if name in shot_names: + duplicate_names.append(name) + else: + shot_names.append(name) + + msg = "There are duplicate shot names:\n{}".format(duplicate_names) + assert not duplicate_names, msg From a88bae8b4b205c64018f46463f331fb9d787b3dd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 10:34:19 +0100 Subject: [PATCH 12/50] Collect shots Shot names are collected from the editorial file instead of sequencial numbering. --- .../standalonepublisher/publish/collect_shots.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/collect_shots.py b/pype/plugins/standalonepublisher/publish/collect_shots.py index 80ee875add..853ba4e8de 100644 --- a/pype/plugins/standalonepublisher/publish/collect_shots.py +++ b/pype/plugins/standalonepublisher/publish/collect_shots.py @@ -60,14 +60,8 @@ class CollectShots(pyblish.api.InstancePlugin): # options to be more flexible. asset_name = asset_name.split("_")[0] - shot_number = 10 + instances = [] for track in tracks: - self.log.info(track) - - if "audio" in track.name.lower(): - continue - - instances = [] for child in track.each_child(): # Transitions are ignored, because Clips have the full frame @@ -75,10 +69,13 @@ class CollectShots(pyblish.api.InstancePlugin): if isinstance(child, otio.schema.transition.Transition): continue + # Hardcoded to expect a shot name of "[name].[extension]" + child_name = os.path.splitext(child.name)[0].lower() + name = f"{asset_name}_{child_name}" + frame_start = child.range_in_parent().start_time.value frame_end = child.range_in_parent().end_time_inclusive().value - name = f"{asset_name}_sh{shot_number:04}" label = f"{name} (framerange: {frame_start}-{frame_end})" instances.append( instance.context.create_instance(**{ @@ -96,8 +93,6 @@ class CollectShots(pyblish.api.InstancePlugin): }) ) - shot_number += 10 - visual_hierarchy = [asset_entity] while True: visual_parent = io.find_one( From d3794478a950f353da9ac04478fca1aff4ea8ba4 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 15:20:44 +0200 Subject: [PATCH 13/50] play, validate buttons are not available on reset but on stop --- pype/tools/pyblish_pype/window.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 5d22e5ac8f..a6e6cd245e 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -726,14 +726,12 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() - self.footer_button_validate.setEnabled(True) - self.footer_button_reset.setEnabled(True) - self.footer_button_stop.setEnabled(False) - self.footer_button_play.setEnabled(True) - self.footer_button_play.setFocus() + self.footer_button_validate.setEnabled(False) + self.footer_button_reset.setEnabled(False) + self.footer_button_stop.setEnabled(True) + self.footer_button_play.setEnabled(False) def on_passed_group(self, order): - for group_item in self.instance_model.group_items.values(): if self.overview_instance_view.isExpanded(group_item.index()): continue @@ -764,11 +762,17 @@ class Window(QtWidgets.QDialog): ) def on_was_stopped(self): - errored = self.controller.errored - self.footer_button_play.setEnabled(not errored) - self.footer_button_validate.setEnabled( - not errored and not self.controller.validated - ) + if self.controller.collected: + errored = self.controller.errored + self.footer_button_play.setEnabled(not errored) + self.footer_button_validate.setEnabled( + not errored and not self.controller.validated + ) + else: + self.footer_button_play.setEnabled(False) + self.footer_button_validate.setEnabled(False) + self.footer_button_play.setFocus() + self.footer_button_reset.setEnabled(True) self.footer_button_stop.setEnabled(False) if errored: From 0c41983e12d52e30d3a3534930de90814ea10436 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 15:22:46 +0200 Subject: [PATCH 14/50] fix missing variable --- pype/tools/pyblish_pype/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index a6e6cd245e..fc5e8cbef5 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -762,8 +762,8 @@ class Window(QtWidgets.QDialog): ) def on_was_stopped(self): + errored = self.controller.errored if self.controller.collected: - errored = self.controller.errored self.footer_button_play.setEnabled(not errored) self.footer_button_validate.setEnabled( not errored and not self.controller.validated From bd195133d38c613db28c23ab3663f005c9f4a9a8 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 17 Jun 2020 15:23:58 +0200 Subject: [PATCH 15/50] switch to default MayaPype plugin, passing more variables, bug fixes --- pype/plugins/global/publish/submit_publish_job.py | 8 ++++++-- pype/plugins/maya/create/create_render.py | 2 +- pype/plugins/maya/publish/collect_render.py | 6 +++--- pype/plugins/maya/publish/submit_maya_deadline.py | 11 +++++++---- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index a05cc3721e..82de2ec099 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -14,7 +14,10 @@ import pyblish.api def _get_script(): """Get path to the image sequence script.""" - from pathlib import Path + try: + from pathlib import Path + except ImportError: + from pathlib2 import Path try: from pype.scripts import publish_filesequence @@ -26,6 +29,7 @@ def _get_script(): module_path = module_path[: -len(".pyc")] + ".py" path = Path(os.path.normpath(module_path)).resolve(strict=True) + assert path is not None, ("Cannot determine path") return str(path) @@ -840,7 +844,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # add audio to metadata file if available audio_file = context.data.get("audioFile") - if os.path.isfile(audio_file): + if audio_file and os.path.isfile(audio_file): publish_job.update({"audio": audio_file}) # pass Ftrack credentials in case of Muster diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index b6eb2e8daa..3b2048d8f0 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -179,7 +179,7 @@ class CreateRender(avalon.maya.Creator): self.data["framesPerTask"] = 1 self.data["whitelist"] = False self.data["machineList"] = "" - self.data["useMayaBatch"] = True + self.data["useMayaBatch"] = False self.data["vrayScene"] = False # Disable for now as this feature is not working yet # self.data["assScene"] = False diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index eed44fbefd..03b14f76bb 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -332,9 +332,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): options["extendFrames"] = extend_frames options["overrideExistingFrame"] = override_frames - maya_render_plugin = "MayaBatch" - if not attributes.get("useMayaBatch", True): - maya_render_plugin = "MayaCmd" + maya_render_plugin = "MayaPype" + if attributes.get("useMayaBatch", True): + maya_render_plugin = "MayaBatch" options["mayaRenderPlugin"] = maya_render_plugin diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 5a8b2f6e5a..8750d88b90 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -41,7 +41,7 @@ payload_skeleton = { "BatchName": None, # Top-level group name "Name": None, # Job name, as seen in Monitor "UserName": None, - "Plugin": "MayaBatch", + "Plugin": "MayaPype", "Frames": "{start}-{end}x{step}", "Comment": None, }, @@ -274,7 +274,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): step=int(self._instance.data["byFrameStep"])) payload_skeleton["JobInfo"]["Plugin"] = self._instance.data.get( - "mayaRenderPlugin", "MayaBatch") + "mayaRenderPlugin", "MayaPype") payload_skeleton["JobInfo"]["BatchName"] = filename # Job name, as seen in Monitor @@ -311,12 +311,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_TASK", "PYPE_USERNAME", "PYPE_DEV", - "PYPE_LOG_NO_COLORS" + "PYPE_LOG_NO_COLORS", + "PYPE_SETUP_PATH" ] environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) environment["PYPE_LOG_NO_COLORS"] = "1" + environment["PYPE_MAYA_VERSION"] = cmds.about(v=True) payload_skeleton["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, @@ -428,7 +430,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): int(self._instance.data["frameStartHandle"]), int(self._instance.data["frameEndHandle"])), - "Plugin": "MayaBatch", + "Plugin": self._instance.data.get( + "mayaRenderPlugin", "MayaPype"), "FramesPerTask": self._instance.data.get("framesPerTask", 1) } From 48085fa8b9cad418c98fe006ded9971a50a2c44b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 15:35:42 +0100 Subject: [PATCH 16/50] Harmony scene validation needs to start at 1. --- pype/plugins/harmony/publish/validate_scene_settings.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pype/plugins/harmony/publish/validate_scene_settings.py b/pype/plugins/harmony/publish/validate_scene_settings.py index 260d64c42b..aa9a70bd85 100644 --- a/pype/plugins/harmony/publish/validate_scene_settings.py +++ b/pype/plugins/harmony/publish/validate_scene_settings.py @@ -31,6 +31,12 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): expected_settings = pype.hosts.harmony.get_asset_settings() + # Harmony is expected to start at 1. + frame_start = expected_settings["frameStart"] + frame_end = expected_settings["frameEnd"] + expected_settings["frameEnd"] = frame_end - frame_start + 1 + expected_settings["frameStart"] = 1 + func = """function func() { return { From 0ab4842c03b0d47e4dfd98d060dd646e2a74a563 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jun 2020 15:37:57 +0100 Subject: [PATCH 17/50] Load audio - Audio on renders (reviews) --- pype/plugins/harmony/load/load_audio.py | 42 +++++++++++++++++++ .../plugins/harmony/publish/extract_render.py | 5 ++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 pype/plugins/harmony/load/load_audio.py diff --git a/pype/plugins/harmony/load/load_audio.py b/pype/plugins/harmony/load/load_audio.py new file mode 100644 index 0000000000..a17af78964 --- /dev/null +++ b/pype/plugins/harmony/load/load_audio.py @@ -0,0 +1,42 @@ +from avalon import api, harmony + + +func = """ +function getUniqueColumnName( column_prefix ) +{ + var suffix = 0; + // finds if unique name for a column + var column_name = column_prefix; + while(suffix < 2000) + { + if(!column.type(column_name)) + break; + + suffix = suffix + 1; + column_name = column_prefix + "_" + suffix; + } + return column_name; +} + +function func(args) +{ + var uniqueColumnName = getUniqueColumnName(args[0]); + column.add(uniqueColumnName , "SOUND"); + column.importSound(uniqueColumnName, 1, args[1]); +} +func +""" + + +class ImportAudioLoader(api.Loader): + """Import audio.""" + + families = ["shot"] + representations = ["wav"] + label = "Import Audio" + + def load(self, context, name=None, namespace=None, data=None): + wav_file = api.get_representation_path(context["representation"]) + harmony.send( + {"function": func, "args": [context["subset"]["name"], wav_file]} + ) diff --git a/pype/plugins/harmony/publish/extract_render.py b/pype/plugins/harmony/publish/extract_render.py index 75d0d2ae36..7ca83d3f0f 100644 --- a/pype/plugins/harmony/publish/extract_render.py +++ b/pype/plugins/harmony/publish/extract_render.py @@ -28,7 +28,8 @@ class ExtractRender(pyblish.api.InstancePlugin): scene.currentScene(), scene.getFrameRate(), scene.getStartFrame(), - scene.getStopFrame() + scene.getStopFrame(), + sound.getSoundtrackAll().path() ] } func @@ -41,6 +42,7 @@ class ExtractRender(pyblish.api.InstancePlugin): frame_rate = result[3] frame_start = result[4] frame_end = result[5] + audio_path = result[6] # Set output path to temp folder. path = tempfile.mkdtemp() @@ -111,6 +113,7 @@ class ExtractRender(pyblish.api.InstancePlugin): mov_path = os.path.join(path, instance.data["name"] + ".mov") args = [ "ffmpeg", "-y", + "-i", audio_path, "-i", os.path.join(path, collection.head + "%04d" + collection.tail), mov_path From 7769560504946937d4b060a720915799ddb278a7 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:20:12 +0200 Subject: [PATCH 18/50] pyblish gui has ability to skip logs with button --- pype/tools/pyblish_pype/model.py | 43 ++++++++++++++++--------------- pype/tools/pyblish_pype/window.py | 11 ++++++-- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index e316d5781a..b9257bfeea 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -1107,35 +1107,36 @@ class TerminalModel(QtGui.QStandardItemModel): def reset(self): self.clear() - def prepare_records(self, result): + def prepare_records(self, result, suspend_logs): prepared_records = [] instance_name = None instance = result["instance"] if instance is not None: instance_name = instance.data["name"] - for record in result.get("records") or []: - if isinstance(record, dict): - record_item = record - else: - record_item = { - "label": text_type(record.msg), - "type": "record", - "levelno": record.levelno, - "threadName": record.threadName, - "name": record.name, - "filename": record.filename, - "pathname": record.pathname, - "lineno": record.lineno, - "msg": text_type(record.msg), - "msecs": record.msecs, - "levelname": record.levelname - } + if not suspend_logs: + for record in result.get("records") or []: + if isinstance(record, dict): + record_item = record + else: + record_item = { + "label": text_type(record.msg), + "type": "record", + "levelno": record.levelno, + "threadName": record.threadName, + "name": record.name, + "filename": record.filename, + "pathname": record.pathname, + "lineno": record.lineno, + "msg": text_type(record.msg), + "msecs": record.msecs, + "levelname": record.levelname + } - if instance_name is not None: - record_item["instance"] = instance_name + if instance_name is not None: + record_item["instance"] = instance_name - prepared_records.append(record_item) + prepared_records.append(record_item) error = result.get("error") if error: diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 701eb41d0d..75b3f31c66 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -54,6 +54,7 @@ class Window(QtWidgets.QDialog): def __init__(self, controller, parent=None): super(Window, self).__init__(parent=parent) + self._suspend_logs = False # Use plastique style for specific ocations # TODO set style name via environment variable low_keys = { @@ -834,7 +835,10 @@ class Window(QtWidgets.QDialog): if self.tabs["artist"].isChecked(): self.tabs["overview"].toggle() - result["records"] = self.terminal_model.prepare_records(result) + result["records"] = self.terminal_model.prepare_records( + result, + self._suspend_logs + ) plugin_item = self.plugin_model.update_with_result(result) instance_item = self.instance_model.update_with_result(result) @@ -933,7 +937,10 @@ class Window(QtWidgets.QDialog): plugin_item = self.plugin_model.plugin_items[result["plugin"].id] action_state = plugin_item.data(Roles.PluginActionProgressRole) action_state |= PluginActionStates.HasFinished - result["records"] = self.terminal_model.prepare_records(result) + result["records"] = self.terminal_model.prepare_records( + result, + self._suspend_logs + ) error = result.get("error") if error: From a3a450330f8516b4bca2a9e28f5054ae883b0e17 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:20:40 +0200 Subject: [PATCH 19/50] added button to skip logs --- pype/tools/pyblish_pype/window.py | 60 +++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 75b3f31c66..9aa77a57a8 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -96,6 +96,18 @@ class Window(QtWidgets.QDialog): header_tab_terminal = QtWidgets.QRadioButton(header_tab_widget) header_spacer = QtWidgets.QWidget(header_tab_widget) + button_suspend_logs_widget = QtWidgets.QWidget() + button_suspend_logs_widget_layout = QtWidgets.QHBoxLayout( + button_suspend_logs_widget + ) + button_suspend_logs_widget_layout.setContentsMargins(0, 10, 0, 10) + button_suspend_logs = QtWidgets.QPushButton(header_widget) + button_suspend_logs.setFixedWidth(7) + button_suspend_logs.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Expanding + ) + button_suspend_logs_widget_layout.addWidget(button_suspend_logs) header_aditional_btns = QtWidgets.QWidget(header_tab_widget) aditional_btns_layout = QtWidgets.QHBoxLayout(header_aditional_btns) @@ -110,9 +122,11 @@ class Window(QtWidgets.QDialog): layout_tab.addWidget(header_tab_artist, 0) layout_tab.addWidget(header_tab_overview, 0) layout_tab.addWidget(header_tab_terminal, 0) + layout_tab.addWidget(button_suspend_logs_widget, 0) + # Compress items to the left layout_tab.addWidget(header_spacer, 1) - layout_tab.addWidget(header_aditional_btns, 1) + layout_tab.addWidget(header_aditional_btns, 0) layout = QtWidgets.QHBoxLayout(header_widget) layout.setContentsMargins(0, 0, 0, 0) @@ -227,6 +241,10 @@ class Window(QtWidgets.QDialog): footer_info = QtWidgets.QLabel(footer_widget) footer_spacer = QtWidgets.QWidget(footer_widget) + + footer_button_stop = QtWidgets.QPushButton( + awesome["stop"], footer_widget + ) footer_button_reset = QtWidgets.QPushButton( awesome["refresh"], footer_widget ) @@ -236,14 +254,12 @@ class Window(QtWidgets.QDialog): footer_button_play = QtWidgets.QPushButton( awesome["play"], footer_widget ) - footer_button_stop = QtWidgets.QPushButton( - awesome["stop"], footer_widget - ) layout = QtWidgets.QHBoxLayout() layout.setContentsMargins(5, 5, 5, 5) layout.addWidget(footer_info, 0) layout.addWidget(footer_spacer, 1) + layout.addWidget(footer_button_stop, 0) layout.addWidget(footer_button_reset, 0) layout.addWidget(footer_button_validate, 0) @@ -343,10 +359,11 @@ class Window(QtWidgets.QDialog): "TerminalView": terminal_view, # Buttons - "Play": footer_button_play, - "Validate": footer_button_validate, - "Reset": footer_button_reset, + "SuspendLogsBtn": button_suspend_logs, "Stop": footer_button_stop, + "Reset": footer_button_reset, + "Validate": footer_button_validate, + "Play": footer_button_play, # Misc "HeaderSpacer": header_spacer, @@ -371,10 +388,11 @@ class Window(QtWidgets.QDialog): overview_page, terminal_page, footer_widget, - footer_button_play, - footer_button_validate, + button_suspend_logs, footer_button_stop, footer_button_reset, + footer_button_validate, + footer_button_play, footer_spacer, closing_placeholder ): @@ -420,6 +438,7 @@ class Window(QtWidgets.QDialog): overview_instance_view.toggled.connect(self.on_instance_toggle) overview_plugin_view.toggled.connect(self.on_plugin_toggle) + button_suspend_logs.clicked.connect(self.on_suspend_clicked) footer_button_stop.clicked.connect(self.on_stop_clicked) footer_button_reset.clicked.connect(self.on_reset_clicked) footer_button_validate.clicked.connect(self.on_validate_clicked) @@ -443,10 +462,11 @@ class Window(QtWidgets.QDialog): self.terminal_filters_widget = terminal_filters_widget self.footer_widget = footer_widget + self.button_suspend_logs = button_suspend_logs + self.footer_button_stop = footer_button_stop self.footer_button_reset = footer_button_reset self.footer_button_validate = footer_button_validate self.footer_button_play = footer_button_play - self.footer_button_stop = footer_button_stop self.overview_instance_view = overview_instance_view self.overview_plugin_view = overview_plugin_view @@ -613,6 +633,13 @@ class Window(QtWidgets.QDialog): self.footer_button_play.setEnabled(False) self.footer_button_stop.setEnabled(False) + def on_suspend_clicked(self): + self._suspend_logs = not self._suspend_logs + if self.state["current_page"] == "terminal": + self.on_tab_changed("overview") + + self.tabs["terminal"].setVisible(not self._suspend_logs) + def on_comment_entered(self): """The user has typed a comment.""" self.controller.context.data["comment"] = self.comment_box.text() @@ -727,6 +754,8 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() + self.button_suspend_logs.setEnabled(False) + self.footer_button_validate.setEnabled(True) self.footer_button_reset.setEnabled(True) self.footer_button_stop.setEnabled(False) @@ -776,6 +805,12 @@ class Window(QtWidgets.QDialog): self.footer_widget.setProperty("success", 0) self.footer_widget.style().polish(self.footer_widget) + suspend_log_bool = ( + self.controller.collect_state == 1 + and not self.controller.stopped + ) + self.button_suspend_logs.setEnabled(suspend_log_bool) + def on_was_skipped(self, plugin): plugin_item = self.plugin_model.plugin_items[plugin.id] plugin_item.setData( @@ -896,16 +931,19 @@ class Window(QtWidgets.QDialog): self.footer_button_validate.setEnabled(False) self.footer_button_play.setEnabled(False) + self.button_suspend_logs.setEnabled(False) + util.defer(5, self.controller.validate) def publish(self): self.info(self.tr("Preparing publish..")) - self.footer_button_stop.setEnabled(True) self.footer_button_reset.setEnabled(False) self.footer_button_validate.setEnabled(False) self.footer_button_play.setEnabled(False) + self.button_suspend_logs.setEnabled(False) + util.defer(5, self.controller.publish) def act(self, plugin_item, action): From 2b29562c9c8a84e36d61c8f397b4b4b06c845e97 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:20:49 +0200 Subject: [PATCH 20/50] button for skipping logs has style --- pype/tools/pyblish_pype/app.css | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pype/tools/pyblish_pype/app.css b/pype/tools/pyblish_pype/app.css index b52d9efec8..3a2c05c1f3 100644 --- a/pype/tools/pyblish_pype/app.css +++ b/pype/tools/pyblish_pype/app.css @@ -491,3 +491,24 @@ QToolButton { #TerminalFilerBtn[type="log_critical"]:checked {color: rgb(255, 79, 117);} #TerminalFilerBtn[type="log_critical"] {color: rgba(255, 79, 117, 63);} + +#SuspendLogsBtn { + background: #444; + border: none; + border-top-right-radius: 7px; + border-bottom-right-radius: 7px; + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; + font-family: "FontAwesome"; + font-size: 11pt; + color: white; + padding: 0px; +} + +#SuspendLogsBtn:hover { + background: #333; +} + +#SuspendLogsBtn:disabled { + background: #4c4c4c; +} From 86fe1bd06a424e403c13986d0c1840bed4acc9f2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 17 Jun 2020 19:21:04 +0200 Subject: [PATCH 21/50] removed unnecessary attribute --- pype/tools/pyblish_pype/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pype/tools/pyblish_pype/control.py b/pype/tools/pyblish_pype/control.py index a078f0146d..5138b5cc4c 100644 --- a/pype/tools/pyblish_pype/control.py +++ b/pype/tools/pyblish_pype/control.py @@ -86,7 +86,6 @@ class Controller(QtCore.QObject): # - passing collectors order disables plugin/instance toggle self.collectors_order = None self.collect_state = 0 - self.collected = False # - passing validators order disables validate button and gives ability # to know when to stop on validate button press From bcdc7c9634f8b8c907de3b4a11b5c07689096ede Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Jun 2020 15:18:00 +0200 Subject: [PATCH 22/50] reapply suspend logs btn --- pype/tools/pyblish_pype/window.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index c9677f0ffd..c88a7e4fd6 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -754,6 +754,8 @@ class Window(QtWidgets.QDialog): self.on_tab_changed(self.state["current_page"]) self.update_compatibility() + self.button_suspend_logs.setEnabled(False) + self.footer_button_validate.setEnabled(False) self.footer_button_reset.setEnabled(False) self.footer_button_stop.setEnabled(True) From 3063ce29c0ffdf1dd8689876bf09a24a2b394ed8 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Jun 2020 15:22:55 +0200 Subject: [PATCH 23/50] changed removed attribute --- pype/tools/pyblish_pype/window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index c88a7e4fd6..8d4a80107c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -793,14 +793,14 @@ class Window(QtWidgets.QDialog): def on_was_stopped(self): errored = self.controller.errored - if self.controller.collected: + if self.controller.collect_state == 0: + self.footer_button_play.setEnabled(False) + self.footer_button_validate.setEnabled(False) + else: self.footer_button_play.setEnabled(not errored) self.footer_button_validate.setEnabled( not errored and not self.controller.validated ) - else: - self.footer_button_play.setEnabled(False) - self.footer_button_validate.setEnabled(False) self.footer_button_play.setFocus() self.footer_button_reset.setEnabled(True) From e73db66e6d32af5b3f05d895ac331c999294ca57 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 18 Jun 2020 18:14:18 +0200 Subject: [PATCH 24/50] use `get_ffmpeg_tool_path` for ffmpeg executable to be sure its used pypes ffmpeg --- pype/plugins/standalonepublisher/publish/extract_shot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pype/plugins/standalonepublisher/publish/extract_shot.py b/pype/plugins/standalonepublisher/publish/extract_shot.py index f2fc2b74cf..d58ddfe8d5 100644 --- a/pype/plugins/standalonepublisher/publish/extract_shot.py +++ b/pype/plugins/standalonepublisher/publish/extract_shot.py @@ -26,8 +26,9 @@ class ExtractShot(pype.api.Extractor): os.path.dirname(editorial_path), basename + ".mov" ) shot_mov = os.path.join(staging_dir, instance.data["name"] + ".mov") + ffmpeg_path = pype.lib.get_ffmpeg_tool_path("ffmpeg") args = [ - "ffmpeg", + ffmpeg_path, "-ss", str(instance.data["frameStart"] / fps), "-i", input_path, "-t", str( @@ -58,7 +59,7 @@ class ExtractShot(pype.api.Extractor): shot_jpegs = os.path.join( staging_dir, instance.data["name"] + ".%04d.jpeg" ) - args = ["ffmpeg", "-i", shot_mov, shot_jpegs] + args = [ffmpeg_path, "-i", shot_mov, shot_jpegs] self.log.info(f"Processing: {args}") output = pype.lib._subprocess(args) self.log.info(output) @@ -79,7 +80,7 @@ class ExtractShot(pype.api.Extractor): # Generate wav file. shot_wav = os.path.join(staging_dir, instance.data["name"] + ".wav") - args = ["ffmpeg", "-i", shot_mov, shot_wav] + args = [ffmpeg_path, "-i", shot_mov, shot_wav] self.log.info(f"Processing: {args}") output = pype.lib._subprocess(args) self.log.info(output) From 511b2d1e24b1d463c18136eecc18f228aa03c7fd Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jun 2020 22:33:29 +0100 Subject: [PATCH 25/50] Cloud Mongo There was a circular import when importing from pypeapp, which I couldnt resolve, so have copied the mongo url logic across. --- pype/api.py | 8 +- pype/lib.py | 81 +++++++++++++++++++ .../adobe_communicator/lib/io_nonsingleton.py | 10 ++- pype/modules/avalon_apps/rest_api.py | 5 +- .../ftrack/ftrack_server/event_server_cli.py | 31 ++++--- pype/modules/ftrack/ftrack_server/lib.py | 72 ++--------------- .../ftrack/ftrack_server/sub_event_storer.py | 1 - .../modules/ftrack/lib/custom_db_connector.py | 15 ++-- pype/modules/ftrack/lib/io_nonsingleton.py | 5 +- schema/session-2.0.json | 7 -- 10 files changed, 131 insertions(+), 104 deletions(-) diff --git a/pype/api.py b/pype/api.py index 200ca9daec..f279bb501a 100644 --- a/pype/api.py +++ b/pype/api.py @@ -32,7 +32,10 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment + add_tool_to_environment, + decompose_url, + compose_url, + get_default_components ) # Special naming case for subprocess since its a built-in method. @@ -44,6 +47,9 @@ __all__ = [ "project_overrides_dir_path", "config", "execute", + "decompose_url", + "compose_url", + "get_default_components", # plugin classes "Extractor", diff --git a/pype/lib.py b/pype/lib.py index 7c7a01d5cc..a41a082bc0 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1386,3 +1386,84 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] + + +import os + +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + + +def decompose_url(url): + components = { + "scheme": None, + "host": None, + "port": None, + "username": None, + "password": None, + "auth_db": "" + } + + result = urlparse(url) + + components["scheme"] = result.scheme + components["host"] = result.hostname + try: + components["port"] = result.port + except ValueError: + raise RuntimeError("invalid port specified") + components["username"] = result.username + components["password"] = result.password + + try: + components["auth_db"] = parse_qs(result.query)['authSource'][0] + except KeyError: + # no auth db provided, mongo will use the one we are connecting to + pass + + return components + + +def compose_url(scheme=None, + host=None, + username=None, + password=None, + database=None, + collection=None, + port=None, + auth_db=""): + + url = "{scheme}://" + + if username and password: + url += "{username}:{password}@" + + url += "{host}" + + if database: + url += "/{database}" + + if database and collection: + url += "/{collection}" + + if port: + url += ":{port}" + + url += auth_db + + return url.format(**{ + "scheme": scheme, + "host": host, + "username": username, + "password": password, + "database": database, + "collection": collection, + "port": port, + "auth_db": "" + }) + + +def get_default_components(): + return decompose_url(os.environ["MONGO_URL"]) diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py index 6380e4eb23..d042d2f6d8 100644 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ b/pype/modules/adobe_communicator/lib/io_nonsingleton.py @@ -16,6 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests +from pype.api import get_default_components, compose_url # Third-party dependencies import pymongo @@ -72,8 +73,15 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) + + components = get_default_components() + port = components.pop("port") + host = compose_url(**components) self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + host=host, + port=port, + serverSelectionTimeoutMS=timeout + ) for retry in range(3): try: diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index a5dc326a8e..58cb3a47f3 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -8,10 +8,7 @@ from pype.modules.ftrack.lib.custom_db_connector import DbConnector class AvalonRestApi(RestApi): - dbcon = DbConnector( - os.environ["AVALON_MONGO"], - os.environ["AVALON_DB"] - ) + dbcon = DbConnector(os.environ["AVALON_DB"]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index 5709a88297..7ca04756df 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -13,10 +13,10 @@ import time import uuid import ftrack_api +import pymongo from pype.modules.ftrack.lib import credentials -from pype.modules.ftrack.ftrack_server.lib import ( - ftrack_events_mongo_settings, check_ftrack_url -) +from pype.modules.ftrack.ftrack_server.lib import check_ftrack_url +from pype.api import get_default_components, compose_url import socket_thread @@ -30,22 +30,19 @@ class MongoPermissionsError(Exception): def check_mongo_url(host, port, log_error=False): """Checks if mongo server is responding""" - sock = None try: - sock = socket.create_connection( - (host, port), - timeout=1 - ) - return True - except socket.error as err: + client = pymongo.MongoClient(host=host, port=port) + # Force connection on a request as the connect=True parameter of + # MongoClient seems to be useless here + client.server_info() + except pymongo.errors.ServerSelectionTimeoutError as err: if log_error: print("Can't connect to MongoDB at {}:{} because: {}".format( host, port, err )) return False - finally: - if sock is not None: - sock.close() + + return True def validate_credentials(url, user, api): @@ -190,9 +187,9 @@ def main_loop(ftrack_url): os.environ["FTRACK_EVENT_SUB_ID"] = str(uuid.uuid1()) # Get mongo hostname and port for testing mongo connection - mongo_list = ftrack_events_mongo_settings() - mongo_hostname = mongo_list[0] - mongo_port = mongo_list[1] + components = get_default_components() + mongo_port = components.pop("port") + mongo_hostname = compose_url(**components) # Current file file_path = os.path.dirname(os.path.realpath(__file__)) @@ -275,7 +272,7 @@ def main_loop(ftrack_url): # Run threads only if Ftrack is accessible if not ftrack_accessible or not mongo_accessible: if not mongo_accessible and not printed_mongo_error: - mongo_url = mongo_hostname + ":" + mongo_port + mongo_url = "{}:{}".format(mongo_hostname, mongo_port) print("Can't access Mongo {}".format(mongo_url)) if not ftrack_accessible and not printed_ftrack_error: diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 129cd7173a..b7383dab07 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -18,12 +18,8 @@ import ftrack_api.operation import ftrack_api._centralized_storage_scenario import ftrack_api.event from ftrack_api.logging import LazyLogMessage as L -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs -from pype.api import Logger +from pype.api import Logger, compose_url, get_default_components from pype.modules.ftrack.lib.custom_db_connector import DbConnector @@ -32,67 +28,10 @@ TOPIC_STATUS_SERVER = "pype.event.server.status" TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" -def ftrack_events_mongo_settings(): - host = None - port = None - username = None - password = None - collection = None - database = None - auth_db = "" - - if os.environ.get('FTRACK_EVENTS_MONGO_URL'): - result = urlparse(os.environ['FTRACK_EVENTS_MONGO_URL']) - - host = result.hostname - try: - port = result.port - except ValueError: - raise RuntimeError("invalid port specified") - username = result.username - password = result.password - try: - database = result.path.lstrip("/").split("/")[0] - collection = result.path.lstrip("/").split("/")[1] - except IndexError: - if not database: - raise RuntimeError("missing database name for logging") - try: - auth_db = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass - else: - host = os.environ.get('FTRACK_EVENTS_MONGO_HOST') - port = int(os.environ.get('FTRACK_EVENTS_MONGO_PORT', "0")) - database = os.environ.get('FTRACK_EVENTS_MONGO_DB') - username = os.environ.get('FTRACK_EVENTS_MONGO_USER') - password = os.environ.get('FTRACK_EVENTS_MONGO_PASSWORD') - collection = os.environ.get('FTRACK_EVENTS_MONGO_COL') - auth_db = os.environ.get('FTRACK_EVENTS_MONGO_AUTH_DB', 'avalon') - - return host, port, database, username, password, collection, auth_db - - def get_ftrack_event_mongo_info(): - host, port, database, username, password, collection, auth_db = ( - ftrack_events_mongo_settings() - ) - user_pass = "" - if username and password: - user_pass = "{}:{}@".format(username, password) - - socket_path = "{}:{}".format(host, port) - - dab = "" - if database: - dab = "/{}".format(database) - - auth = "" - if auth_db: - auth = "?authSource={}".format(auth_db) - - url = "mongodb://{}{}{}{}".format(user_pass, socket_path, dab, auth) + url = compose_url(get_default_components()) + database = os.environ["FTRACK_EVENTS_MONGO_DB"] + collection = os.environ["FTRACK_EVENTS_MONGO_COL"] return url, database, collection @@ -205,7 +144,6 @@ class ProcessEventHub(SocketBaseEventHub): def __init__(self, *args, **kwargs): self.dbcon = DbConnector( - mongo_url=self.url, database_name=self.database, table_name=self.table_name ) @@ -269,7 +207,7 @@ class ProcessEventHub(SocketBaseEventHub): def load_events(self): """Load not processed events sorted by stored date""" ago_date = datetime.datetime.now() - datetime.timedelta(days=3) - result = self.dbcon.delete_many({ + self.dbcon.delete_many({ "pype_data.stored": {"$lte": ago_date}, "pype_data.is_processed": True }) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index c4d199407d..727d5aa515 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -25,7 +25,6 @@ class SessionFactory: url, database, table_name = get_ftrack_event_mongo_info() dbcon = DbConnector( - mongo_url=url, database_name=database, table_name=table_name ) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index b307117127..e570e4da38 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -13,6 +13,8 @@ import atexit # Third-party dependencies import pymongo +from pype.api import get_default_components, compose_url + class NotActiveTable(Exception): def __init__(self, *args, **kwargs): @@ -63,13 +65,12 @@ class DbConnector: log = logging.getLogger(__name__) timeout = 1000 - def __init__(self, mongo_url, database_name, table_name=None): + def __init__(self, database_name, table_name=None): self._mongo_client = None self._sentry_client = None self._sentry_logging_handler = None self._database = None self._is_installed = False - self._mongo_url = mongo_url self._database_name = database_name self.active_table = table_name @@ -95,8 +96,12 @@ class DbConnector: atexit.register(self.uninstall) logging.basicConfig() + components = get_default_components() + port = components.pop("port") + host = compose_url(**components) self._mongo_client = pymongo.MongoClient( - self._mongo_url, + host=host, + port=port, serverSelectionTimeoutMS=self.timeout ) @@ -113,11 +118,11 @@ class DbConnector: else: raise IOError( "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (self._mongo_url, self.timeout) + "less than %.3f ms" % (host, self.timeout) ) self.log.info("Connected to %s, delay %.3f s" % ( - self._mongo_url, time.time() - t1 + host, time.time() - t1 )) self._database = self._mongo_client[self._database_name] diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py index 6380e4eb23..73856557ea 100644 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ b/pype/modules/ftrack/lib/io_nonsingleton.py @@ -73,7 +73,10 @@ class DbConnector(object): timeout = int(self.Session["AVALON_TIMEOUT"]) self._mongo_client = pymongo.MongoClient( - self.Session["AVALON_MONGO"], serverSelectionTimeoutMS=timeout) + host=os.environ["AVALON_MONGO_HOST"], + port=int(os.environ["AVALON_MONGO_PORT"]), + serverSelectionTimeoutMS=timeout + ) for retry in range(3): try: diff --git a/schema/session-2.0.json b/schema/session-2.0.json index d37f2ac822..7ad2c63bcf 100644 --- a/schema/session-2.0.json +++ b/schema/session-2.0.json @@ -56,13 +56,6 @@ "pattern": "^\\w*$", "example": "maya2016" }, - "AVALON_MONGO": { - "description": "Address to the asset database", - "type": "string", - "pattern": "^mongodb://[\\w/@:.]*$", - "example": "mongodb://localhost:27017", - "default": "mongodb://localhost:27017" - }, "AVALON_DB": { "description": "Name of database", "type": "string", From c3bf82ac1c9ed63acda481a557d5e3c03a04f77a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jun 2020 22:37:45 +0100 Subject: [PATCH 26/50] Hound --- pype/lib.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index a41a082bc0..1323897d37 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -17,6 +17,11 @@ import six import avalon.api from .api import config +try: + from urllib.parse import urlparse, parse_qs +except ImportError: + from urlparse import urlparse, parse_qs + log = logging.getLogger(__name__) @@ -1388,14 +1393,6 @@ def ffprobe_streams(path_to_file): return json.loads(popen_output)["streams"] -import os - -try: - from urllib.parse import urlparse, parse_qs -except ImportError: - from urlparse import urlparse, parse_qs - - def decompose_url(url): components = { "scheme": None, From e04345480549cdfc302117e34165d8324f1907d6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jun 2020 22:57:43 +0100 Subject: [PATCH 27/50] Make ftrack database and collection optional. --- pype/modules/ftrack/ftrack_server/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index b7383dab07..742976104e 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -30,8 +30,8 @@ TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" def get_ftrack_event_mongo_info(): url = compose_url(get_default_components()) - database = os.environ["FTRACK_EVENTS_MONGO_DB"] - collection = os.environ["FTRACK_EVENTS_MONGO_COL"] + database = os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" + collection = os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" return url, database, collection From 335b1fecaac6fd5f83a33b6f18fefeaf3aca01d9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 08:57:48 +0100 Subject: [PATCH 28/50] Improve query --- pype/lib.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 1323897d37..ef70aa59cd 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -1400,7 +1400,7 @@ def decompose_url(url): "port": None, "username": None, "password": None, - "auth_db": "" + "query": None } result = urlparse(url) @@ -1411,14 +1411,10 @@ def decompose_url(url): components["port"] = result.port except ValueError: raise RuntimeError("invalid port specified") + components["username"] = result.username components["password"] = result.password - - try: - components["auth_db"] = parse_qs(result.query)['authSource'][0] - except KeyError: - # no auth db provided, mongo will use the one we are connecting to - pass + components["query"] = result.query return components @@ -1430,7 +1426,7 @@ def compose_url(scheme=None, database=None, collection=None, port=None, - auth_db=""): + query=None): url = "{scheme}://" @@ -1448,7 +1444,8 @@ def compose_url(scheme=None, if port: url += ":{port}" - url += auth_db + if query: + url += "?{}".format(query) return url.format(**{ "scheme": scheme, @@ -1458,7 +1455,7 @@ def compose_url(scheme=None, "database": database, "collection": collection, "port": port, - "auth_db": "" + "query": query }) From 3c3a9bb780c57111c8f35c5a2fb31a1e58b76711 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 08:58:39 +0100 Subject: [PATCH 29/50] Hound --- pype/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index ef70aa59cd..540a28afc3 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -18,9 +18,9 @@ import avalon.api from .api import config try: - from urllib.parse import urlparse, parse_qs + from urllib.parse import urlparse except ImportError: - from urlparse import urlparse, parse_qs + from urlparse import urlparse log = logging.getLogger(__name__) From f3a435ee400d44c9e79f74431185f985d6486559 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 09:28:04 +0100 Subject: [PATCH 30/50] Increment workfile and sneaky code cosmetic for Harmony. --- .../harmony/publish/increment_workfile.py | 3 +- .../photoshop/publish/increment_workfile.py | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 pype/plugins/photoshop/publish/increment_workfile.py diff --git a/pype/plugins/harmony/publish/increment_workfile.py b/pype/plugins/harmony/publish/increment_workfile.py index 29bae09df3..858e5fab0e 100644 --- a/pype/plugins/harmony/publish/increment_workfile.py +++ b/pype/plugins/harmony/publish/increment_workfile.py @@ -22,8 +22,7 @@ class IncrementWorkfile(pyblish.api.InstancePlugin): errored_plugins = get_errored_plugins_from_data(instance.context) if errored_plugins: raise RuntimeError( - "Skipping incrementing current file because submission to" - " deadline failed." + "Skipping incrementing current file because publishing failed." ) scene_dir = version_up( diff --git a/pype/plugins/photoshop/publish/increment_workfile.py b/pype/plugins/photoshop/publish/increment_workfile.py new file mode 100644 index 0000000000..ba9ab8606a --- /dev/null +++ b/pype/plugins/photoshop/publish/increment_workfile.py @@ -0,0 +1,29 @@ +import pyblish.api +from pype.action import get_errored_plugins_from_data +from pype.lib import version_up +from avalon import photoshop + + +class IncrementWorkfile(pyblish.api.InstancePlugin): + """Increment the current workfile. + + Saves the current scene with an increased version number. + """ + + label = "Increment Workfile" + order = pyblish.api.IntegratorOrder + 9.0 + hosts = ["photoshop"] + families = ["workfile"] + optional = True + + def process(self, instance): + errored_plugins = get_errored_plugins_from_data(instance.context) + if errored_plugins: + raise RuntimeError( + "Skipping incrementing current file because publishing failed." + ) + + scene_path = version_up(instance.context.data["currentFile"]) + photoshop.app().ActiveDocument.SaveAs(scene_path) + + self.log.info("Incremented workfile to: {}".format(scene_path)) From ac78278a950d1ed0d0569e60f1fbb24f346e4e11 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 12:18:03 +0100 Subject: [PATCH 31/50] Defaults for frameStart and frameEnd Only warn users about missing attributes. --- .../global/publish/collect_avalon_entities.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/pype/plugins/global/publish/collect_avalon_entities.py b/pype/plugins/global/publish/collect_avalon_entities.py index 51dd3d7b06..917172d40c 100644 --- a/pype/plugins/global/publish/collect_avalon_entities.py +++ b/pype/plugins/global/publish/collect_avalon_entities.py @@ -48,8 +48,18 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): data = asset_entity['data'] - context.data["frameStart"] = data.get("frameStart") - context.data["frameEnd"] = data.get("frameEnd") + frame_start = data.get("frameStart") + if frame_start is None: + frame_start = 1 + self.log.warning("Missing frame start. Defaulting to 1.") + + frame_end = data.get("frameEnd") + if frame_end is None: + frame_end = 2 + self.log.warning("Missing frame end. Defaulting to 2.") + + context.data["frameStart"] = frame_start + context.data["frameEnd"] = frame_end handles = data.get("handles") or 0 handle_start = data.get("handleStart") @@ -72,7 +82,7 @@ class CollectAvalonEntities(pyblish.api.ContextPlugin): context.data["handleStart"] = int(handle_start) context.data["handleEnd"] = int(handle_end) - frame_start_h = data.get("frameStart") - context.data["handleStart"] - frame_end_h = data.get("frameEnd") + context.data["handleEnd"] + frame_start_h = frame_start - context.data["handleStart"] + frame_end_h = frame_end + context.data["handleEnd"] context.data["frameStartHandle"] = frame_start_h context.data["frameEndHandle"] = frame_end_h From eaf003cbd60386c538c28deb982bd018280a510c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 14:27:41 +0200 Subject: [PATCH 32/50] widgets are imported in view.py when are used --- pype/tools/pyblish_pype/view.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/view.py b/pype/tools/pyblish_pype/view.py index 03509604bb..450f56421c 100644 --- a/pype/tools/pyblish_pype/view.py +++ b/pype/tools/pyblish_pype/view.py @@ -1,7 +1,14 @@ from Qt import QtCore, QtWidgets from . import model -from . import widgets from .constants import Roles +# Imported when used +widgets = None + + +def _import_widgets(): + global widgets + if widgets is None: + from . import widgets class ArtistView(QtWidgets.QListView): @@ -152,6 +159,8 @@ class TerminalView(QtWidgets.QTreeView): self.clicked.connect(self.item_expand) + _import_widgets() + def event(self, event): if not event.type() == QtCore.QEvent.KeyPress: return super(TerminalView, self).event(event) From 36aee8395bbfc893c8e8fd7c18a0de418b7d8497 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 15:32:02 +0200 Subject: [PATCH 33/50] warnings visualizaion is working again --- pype/tools/pyblish_pype/model.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/pype/tools/pyblish_pype/model.py b/pype/tools/pyblish_pype/model.py index b9257bfeea..203b512d12 100644 --- a/pype/tools/pyblish_pype/model.py +++ b/pype/tools/pyblish_pype/model.py @@ -490,12 +490,8 @@ class PluginModel(QtGui.QStandardItemModel): new_records = result.get("records") or [] if not has_warning: for record in new_records: - if not hasattr(record, "levelname"): - continue - - if str(record.levelname).lower() in [ - "warning", "critical", "error" - ]: + level_no = record.get("levelno") + if level_no and level_no >= 30: new_flag_states[PluginStates.HasWarning] = True break @@ -789,12 +785,8 @@ class InstanceModel(QtGui.QStandardItemModel): new_records = result.get("records") or [] if not has_warning: for record in new_records: - if not hasattr(record, "levelname"): - continue - - if str(record.levelname).lower() in [ - "warning", "critical", "error" - ]: + level_no = record.get("levelno") + if level_no and level_no >= 30: new_flag_states[InstanceStates.HasWarning] = True break From c83b939bfff8f63deab440c0d733e4924d998ec3 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 15:32:38 +0200 Subject: [PATCH 34/50] appending error message to logs is skipped in action handler because is already done in terminal model --- pype/tools/pyblish_pype/window.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 8d4a80107c..44472f8e1d 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -984,22 +984,8 @@ class Window(QtWidgets.QDialog): self._suspend_logs ) - error = result.get("error") - if error: - records = result.get("records") or [] + if "error" in result: action_state |= PluginActionStates.HasFailed - fname, line_no, func, exc = error.traceback - - records.append({ - "label": str(error), - "type": "error", - "filename": str(fname), - "lineno": str(line_no), - "func": str(func), - "traceback": error.formatted_traceback - }) - - result["records"] = records plugin_item.setData(action_state, Roles.PluginActionProgressRole) From e33999ef486e0c988b8033667bdc34f6590710b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Fri, 19 Jun 2020 15:32:57 +0200 Subject: [PATCH 35/50] perspective widget is updated after action processing --- pype/tools/pyblish_pype/window.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 44472f8e1d..2967488572 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -989,9 +989,14 @@ class Window(QtWidgets.QDialog): plugin_item.setData(action_state, Roles.PluginActionProgressRole) - self.plugin_model.update_with_result(result) - self.instance_model.update_with_result(result) self.terminal_model.update_with_result(result) + plugin_item = self.plugin_model.update_with_result(result) + instance_item = self.instance_model.update_with_result(result) + + if self.perspective_widget.isVisible(): + self.perspective_widget.update_context( + plugin_item, instance_item + ) def closeEvent(self, event): """Perform post-flight checks before closing From 9b9063b7d1450e30ec66b8b7323a22d9e392a51a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 19 Jun 2020 17:09:27 +0100 Subject: [PATCH 36/50] Fix logging --- pype/modules/logging/gui/models.py | 8 +++----- pype/modules/logging/tray/logging_module.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 484fd6dc69..945b76152b 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,8 +1,7 @@ -import os import collections from Qt import QtCore from pype.api import Logger -from pypeapp.lib.log import _bootstrap_mongo_log +from pypeapp.lib.log import _bootstrap_mongo_log, COLLECTION log = Logger().get_logger("LogModel", "LoggingModule") @@ -41,11 +40,10 @@ class LogModel(QtCore.QAbstractItemModel): super(LogModel, self).__init__(parent) self._root_node = Node() - collection = os.environ.get('PYPE_LOG_MONGO_COL') database = _bootstrap_mongo_log() self.dbcon = None - if collection in database.list_collection_names(): - self.dbcon = database[collection] + if COLLECTION in database.list_collection_names(): + self.dbcon = database[COLLECTION] def add_log(self, log): node = Node(log) diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py index 087a51f322..9b26d5d9bf 100644 --- a/pype/modules/logging/tray/logging_module.py +++ b/pype/modules/logging/tray/logging_module.py @@ -1,20 +1,23 @@ from Qt import QtWidgets - from pype.api import Logger - from ..gui.app import LogsWindow -log = Logger().get_logger("LoggingModule", "logging") - class LoggingModule: def __init__(self, main_parent=None, parent=None): self.parent = parent + self.log = Logger().get_logger(self.__class__.__name__, "logging") - self.window = LogsWindow() + try: + self.window = LogsWindow() + self.tray_menu = self._tray_menu + except Exception: + self.log.warning( + "Couldn't set Logging GUI due to error.", exc_info=True + ) # Definition of Tray menu - def tray_menu(self, parent_menu): + def _tray_menu(self, parent_menu): # Menu for Tray App menu = QtWidgets.QMenu('Logging', parent_menu) # menu.setProperty('submenu', 'on') From d665d822ae150c8e8d3c662d0522ebea52b0e3b1 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:45:03 +0200 Subject: [PATCH 37/50] removed decompose_url, compose_url and get_default_components from pype.lib because are duplicated with pypeapp --- pype/lib.py | 74 ----------------------------------------------------- 1 file changed, 74 deletions(-) diff --git a/pype/lib.py b/pype/lib.py index 540a28afc3..87808e53f5 100644 --- a/pype/lib.py +++ b/pype/lib.py @@ -17,10 +17,6 @@ import six import avalon.api from .api import config -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse log = logging.getLogger(__name__) @@ -1391,73 +1387,3 @@ def ffprobe_streams(path_to_file): popen_output = popen.communicate()[0] log.debug("FFprobe output: {}".format(popen_output)) return json.loads(popen_output)["streams"] - - -def decompose_url(url): - components = { - "scheme": None, - "host": None, - "port": None, - "username": None, - "password": None, - "query": None - } - - result = urlparse(url) - - components["scheme"] = result.scheme - components["host"] = result.hostname - try: - components["port"] = result.port - except ValueError: - raise RuntimeError("invalid port specified") - - components["username"] = result.username - components["password"] = result.password - components["query"] = result.query - - return components - - -def compose_url(scheme=None, - host=None, - username=None, - password=None, - database=None, - collection=None, - port=None, - query=None): - - url = "{scheme}://" - - if username and password: - url += "{username}:{password}@" - - url += "{host}" - - if database: - url += "/{database}" - - if database and collection: - url += "/{collection}" - - if port: - url += ":{port}" - - if query: - url += "?{}".format(query) - - return url.format(**{ - "scheme": scheme, - "host": host, - "username": username, - "password": password, - "database": database, - "collection": collection, - "port": port, - "query": query - }) - - -def get_default_components(): - return decompose_url(os.environ["MONGO_URL"]) From 1c31e768f36eda4414361eb450d37134e40c621f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:45:23 +0200 Subject: [PATCH 38/50] decompose_url, compose_url, get_default_components are imported from pypeapp in pype.api --- pype/api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pype/api.py b/pype/api.py index f279bb501a..4b3d50044f 100644 --- a/pype/api.py +++ b/pype/api.py @@ -6,6 +6,12 @@ from pypeapp import ( execute ) +from pypeapp.lib.mongo import ( + decompose_url, + compose_url, + get_default_components +) + from .plugin import ( Extractor, @@ -32,10 +38,7 @@ from .lib import ( get_version_from_path, get_last_version_from_path, modified_environ, - add_tool_to_environment, - decompose_url, - compose_url, - get_default_components + add_tool_to_environment ) # Special naming case for subprocess since its a built-in method. @@ -49,7 +52,6 @@ __all__ = [ "execute", "decompose_url", "compose_url", - "get_default_components", # plugin classes "Extractor", From b4b3ed4bf44dd1d11178ab1d43ec6e037378ba16 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:49:19 +0200 Subject: [PATCH 39/50] custom db connector can expect different mongo url --- .../modules/ftrack/lib/custom_db_connector.py | 44 +++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/pype/modules/ftrack/lib/custom_db_connector.py b/pype/modules/ftrack/lib/custom_db_connector.py index e570e4da38..a734b3f80a 100644 --- a/pype/modules/ftrack/lib/custom_db_connector.py +++ b/pype/modules/ftrack/lib/custom_db_connector.py @@ -12,8 +12,7 @@ import atexit # Third-party dependencies import pymongo - -from pype.api import get_default_components, compose_url +from pype.api import decompose_url class NotActiveTable(Exception): @@ -65,12 +64,29 @@ class DbConnector: log = logging.getLogger(__name__) timeout = 1000 - def __init__(self, database_name, table_name=None): + def __init__( + self, uri, port=None, database_name=None, table_name=None + ): self._mongo_client = None self._sentry_client = None self._sentry_logging_handler = None self._database = None self._is_installed = False + + self._uri = uri + components = decompose_url(uri) + if port is None: + port = components.get("port") + + if database_name is None: + database_name = components.get("database") + + if database_name is None: + raise ValueError( + "Database is not defined for connection. {}".format(uri) + ) + + self._port = port self._database_name = database_name self.active_table = table_name @@ -96,14 +112,16 @@ class DbConnector: atexit.register(self.uninstall) logging.basicConfig() - components = get_default_components() - port = components.pop("port") - host = compose_url(**components) - self._mongo_client = pymongo.MongoClient( - host=host, - port=port, - serverSelectionTimeoutMS=self.timeout - ) + kwargs = { + "host": self._uri, + "serverSelectionTimeoutMS": self.timeout + } + if self._port is not None: + kwargs["port"] = self._port + + self._mongo_client = pymongo.MongoClient(**kwargs) + if self._port is None: + self._port = self._mongo_client.PORT for retry in range(3): try: @@ -118,11 +136,11 @@ class DbConnector: else: raise IOError( "ERROR: Couldn't connect to %s in " - "less than %.3f ms" % (host, self.timeout) + "less than %.3f ms" % (self._uri, self.timeout) ) self.log.info("Connected to %s, delay %.3f s" % ( - host, time.time() - t1 + self._uri, time.time() - t1 )) self._database = self._mongo_client[self._database_name] From d185b5d54c4364846c884a15cf5c4e0d04be0542 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:52:04 +0200 Subject: [PATCH 40/50] io_nonsingleton is same as avalon's io --- .../adobe_communicator/lib/io_nonsingleton.py | 24 ++++++++++++------- pype/modules/ftrack/lib/io_nonsingleton.py | 21 ++++++++++++---- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/pype/modules/adobe_communicator/lib/io_nonsingleton.py b/pype/modules/adobe_communicator/lib/io_nonsingleton.py index d042d2f6d8..da37c657c6 100644 --- a/pype/modules/adobe_communicator/lib/io_nonsingleton.py +++ b/pype/modules/adobe_communicator/lib/io_nonsingleton.py @@ -16,7 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests -from pype.api import get_default_components, compose_url +from avalon.io import extract_port_from_url # Third-party dependencies import pymongo @@ -73,15 +73,17 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) + mongo_url = self.Session["AVALON_MONGO"] + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } - components = get_default_components() - port = components.pop("port") - host = compose_url(**components) - self._mongo_client = pymongo.MongoClient( - host=host, - port=port, - serverSelectionTimeoutMS=timeout - ) + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + self._mongo_client = pymongo.MongoClient(**kwargs) for retry in range(3): try: @@ -389,6 +391,10 @@ class DbConnector(object): if document is None: break + if document.get("type") == "master_version": + _document = self.find_one({"_id": document["version_id"]}) + document["data"] = _document["data"] + parents.append(document) return parents diff --git a/pype/modules/ftrack/lib/io_nonsingleton.py b/pype/modules/ftrack/lib/io_nonsingleton.py index 73856557ea..da37c657c6 100644 --- a/pype/modules/ftrack/lib/io_nonsingleton.py +++ b/pype/modules/ftrack/lib/io_nonsingleton.py @@ -16,6 +16,7 @@ import contextlib from avalon import schema from avalon.vendor import requests +from avalon.io import extract_port_from_url # Third-party dependencies import pymongo @@ -72,11 +73,17 @@ class DbConnector(object): self.Session.update(self._from_environment()) timeout = int(self.Session["AVALON_TIMEOUT"]) - self._mongo_client = pymongo.MongoClient( - host=os.environ["AVALON_MONGO_HOST"], - port=int(os.environ["AVALON_MONGO_PORT"]), - serverSelectionTimeoutMS=timeout - ) + mongo_url = self.Session["AVALON_MONGO"] + kwargs = { + "host": mongo_url, + "serverSelectionTimeoutMS": timeout + } + + port = extract_port_from_url(mongo_url) + if port is not None: + kwargs["port"] = int(port) + + self._mongo_client = pymongo.MongoClient(**kwargs) for retry in range(3): try: @@ -384,6 +391,10 @@ class DbConnector(object): if document is None: break + if document.get("type") == "master_version": + _document = self.find_one({"_id": document["version_id"]}) + document["data"] = _document["data"] + parents.append(document) return parents From 3148115f0a079688f47907a5dd2df70aba8ab091 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:53:10 +0200 Subject: [PATCH 41/50] avalon rest api use io_nonsingleton instead of custom_db_connector --- pype/modules/avalon_apps/rest_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pype/modules/avalon_apps/rest_api.py b/pype/modules/avalon_apps/rest_api.py index 58cb3a47f3..1cb9e544a7 100644 --- a/pype/modules/avalon_apps/rest_api.py +++ b/pype/modules/avalon_apps/rest_api.py @@ -4,14 +4,14 @@ import json import bson import bson.json_util from pype.modules.rest_api import RestApi, abort, CallbackResult -from pype.modules.ftrack.lib.custom_db_connector import DbConnector +from pype.modules.ftrack.lib.io_nonsingleton import DbConnector class AvalonRestApi(RestApi): - dbcon = DbConnector(os.environ["AVALON_DB"]) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.dbcon = DbConnector() self.dbcon.install() @RestApi.route("/projects/", url_prefix="/avalon", methods="GET") From 4104397f22f7308fa13ba14605102abf981b561f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:53:24 +0200 Subject: [PATCH 42/50] fixed logging gui variables --- pype/modules/logging/gui/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 945b76152b..ce1fa236a9 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,7 +1,7 @@ import collections from Qt import QtCore from pype.api import Logger -from pypeapp.lib.log import _bootstrap_mongo_log, COLLECTION +from pypeapp.lib.log import _bootstrap_mongo_log, LOG_COLLECTION_NAME log = Logger().get_logger("LogModel", "LoggingModule") @@ -40,10 +40,11 @@ class LogModel(QtCore.QAbstractItemModel): super(LogModel, self).__init__(parent) self._root_node = Node() - database = _bootstrap_mongo_log() self.dbcon = None - if COLLECTION in database.list_collection_names(): - self.dbcon = database[COLLECTION] + # Crash if connection is not possible to skip this module + database = _bootstrap_mongo_log() + if LOG_COLLECTION_NAME in database.list_collection_names(): + self.dbcon = database[LOG_COLLECTION_NAME] def add_log(self, log): node = Node(log) From 9089aaafd71215c3c0aa9c006feb0eb9664431c0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:53:47 +0200 Subject: [PATCH 43/50] event storer connection fixed usage --- pype/modules/ftrack/ftrack_server/sub_event_storer.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/sub_event_storer.py b/pype/modules/ftrack/ftrack_server/sub_event_storer.py index 727d5aa515..61b9aaf2c8 100644 --- a/pype/modules/ftrack/ftrack_server/sub_event_storer.py +++ b/pype/modules/ftrack/ftrack_server/sub_event_storer.py @@ -23,11 +23,8 @@ class SessionFactory: session = None -url, database, table_name = get_ftrack_event_mongo_info() -dbcon = DbConnector( - database_name=database, - table_name=table_name -) +uri, port, database, table_name = get_ftrack_event_mongo_info() +dbcon = DbConnector(uri, port, database, table_name) # ignore_topics = ["ftrack.meta.connected"] ignore_topics = [] From c30ba2d86b0ecbe200acb275f38611e304836927 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:55:03 +0200 Subject: [PATCH 44/50] ftrack event may have custom mongo connection --- pype/modules/ftrack/ftrack_server/lib.py | 40 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/lib.py b/pype/modules/ftrack/ftrack_server/lib.py index 742976104e..327fab817d 100644 --- a/pype/modules/ftrack/ftrack_server/lib.py +++ b/pype/modules/ftrack/ftrack_server/lib.py @@ -19,7 +19,12 @@ import ftrack_api._centralized_storage_scenario import ftrack_api.event from ftrack_api.logging import LazyLogMessage as L -from pype.api import Logger, compose_url, get_default_components +from pype.api import ( + Logger, + get_default_components, + decompose_url, + compose_url +) from pype.modules.ftrack.lib.custom_db_connector import DbConnector @@ -29,11 +34,28 @@ TOPIC_STATUS_SERVER_RESULT = "pype.event.server.status.result" def get_ftrack_event_mongo_info(): - url = compose_url(get_default_components()) - database = os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" - collection = os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" + database_name = ( + os.environ.get("FTRACK_EVENTS_MONGO_DB") or "pype" + ) + collection_name = ( + os.environ.get("FTRACK_EVENTS_MONGO_COL") or "ftrack_events" + ) - return url, database, collection + mongo_url = os.environ.get("FTRACK_EVENTS_MONGO_URL") + if mongo_url is not None: + components = decompose_url(mongo_url) + _used_ftrack_url = True + else: + components = get_default_components() + _used_ftrack_url = False + + if not _used_ftrack_url or components["database"] is None: + components["database"] = database_name + components["collection"] = collection_name + + uri = compose_url(components) + + return uri, components["port"], database_name, collection_name def check_ftrack_url(url, log_errors=True): @@ -137,15 +159,17 @@ class StorerEventHub(SocketBaseEventHub): class ProcessEventHub(SocketBaseEventHub): hearbeat_msg = b"processor" - url, database, table_name = get_ftrack_event_mongo_info() + uri, port, database, table_name = get_ftrack_event_mongo_info() is_table_created = False pypelog = Logger().get_logger("Session Processor") def __init__(self, *args, **kwargs): self.dbcon = DbConnector( - database_name=self.database, - table_name=self.table_name + self.uri, + self.port, + self.database, + self.table_name ) super(ProcessEventHub, self).__init__(*args, **kwargs) From e781888d5adf86ebba0a46d65599c18865d83b0f Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 15:56:09 +0200 Subject: [PATCH 45/50] changed event server cli to can use new access to mongo --- .../ftrack/ftrack_server/event_server_cli.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pype/modules/ftrack/ftrack_server/event_server_cli.py b/pype/modules/ftrack/ftrack_server/event_server_cli.py index 7ca04756df..73c7abfc5d 100644 --- a/pype/modules/ftrack/ftrack_server/event_server_cli.py +++ b/pype/modules/ftrack/ftrack_server/event_server_cli.py @@ -15,8 +15,10 @@ import uuid import ftrack_api import pymongo from pype.modules.ftrack.lib import credentials -from pype.modules.ftrack.ftrack_server.lib import check_ftrack_url -from pype.api import get_default_components, compose_url +from pype.modules.ftrack.ftrack_server.lib import ( + check_ftrack_url, get_ftrack_event_mongo_info +) + import socket_thread @@ -187,9 +189,10 @@ def main_loop(ftrack_url): os.environ["FTRACK_EVENT_SUB_ID"] = str(uuid.uuid1()) # Get mongo hostname and port for testing mongo connection - components = get_default_components() - mongo_port = components.pop("port") - mongo_hostname = compose_url(**components) + + mongo_uri, mongo_port, database_name, collection_name = ( + get_ftrack_event_mongo_info() + ) # Current file file_path = os.path.dirname(os.path.realpath(__file__)) @@ -267,13 +270,12 @@ def main_loop(ftrack_url): ftrack_accessible = check_ftrack_url(ftrack_url) if not mongo_accessible: - mongo_accessible = check_mongo_url(mongo_hostname, mongo_port) + mongo_accessible = check_mongo_url(mongo_uri, mongo_port) # Run threads only if Ftrack is accessible if not ftrack_accessible or not mongo_accessible: if not mongo_accessible and not printed_mongo_error: - mongo_url = "{}:{}".format(mongo_hostname, mongo_port) - print("Can't access Mongo {}".format(mongo_url)) + print("Can't access Mongo {}".format(mongo_uri)) if not ftrack_accessible and not printed_ftrack_error: print("Can't access Ftrack {}".format(ftrack_url)) From cd5859cf30b295723ed53f7c204932740b99e3cd Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Sun, 21 Jun 2020 16:13:29 +0200 Subject: [PATCH 46/50] add `get_default_components` to __all__ in pype.api --- pype/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/api.py b/pype/api.py index 4b3d50044f..5775bb3ce4 100644 --- a/pype/api.py +++ b/pype/api.py @@ -52,6 +52,7 @@ __all__ = [ "execute", "decompose_url", "compose_url", + "get_default_components", # plugin classes "Extractor", From db4ab28113b5dde2c8f51f5f2787047e9a0fb5c0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Jun 2020 15:21:23 +0100 Subject: [PATCH 47/50] Using existing folders for instances. --- pype/plugins/photoshop/create/create_image.py | 73 ++++++++++++++++++- .../photoshop/publish/validate_naming.py | 47 ++++++++++++ 2 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 pype/plugins/photoshop/publish/validate_naming.py diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py index a840dd13a7..ff0a5dcb6c 100644 --- a/pype/plugins/photoshop/create/create_image.py +++ b/pype/plugins/photoshop/create/create_image.py @@ -1,12 +1,77 @@ -from avalon import photoshop +from avalon import api, photoshop +from avalon.vendor import Qt -class CreateImage(photoshop.Creator): +class CreateImage(api.Creator): """Image folder for publish.""" name = "imageDefault" label = "Image" family = "image" - def __init__(self, *args, **kwargs): - super(CreateImage, self).__init__(*args, **kwargs) + def process(self): + groups = [] + layers = [] + create_group = False + group_constant = photoshop.get_com_objects().constants().psLayerSet + if (self.options or {}).get("useSelection"): + multiple_instances = False + selection = photoshop.get_selected_layers() + + if len(selection) > 1: + # Ask user whether to create one image or image per selected + # item. + msg_box = Qt.QtWidgets.QMessageBox() + msg_box.setIcon(Qt.QtWidgets.QMessageBox.Warning) + msg_box.setText( + "Multiple layers selected." + "\nDo you want to make one image per layer?" + ) + msg_box.setStandardButtons( + Qt.QtWidgets.QMessageBox.Yes | + Qt.QtWidgets.QMessageBox.No | + Qt.QtWidgets.QMessageBox.Cancel + ) + ret = msg_box.exec_() + if ret == Qt.QtWidgets.QMessageBox.Yes: + multiple_instances = True + elif ret == Qt.QtWidgets.QMessageBox.Cancel: + return + + if multiple_instances: + for item in selection: + if item.LayerType == group_constant: + groups.append(item) + else: + layers.append(item) + else: + group = photoshop.group_selected_layers() + group.Name = self.name + groups.append(group) + + elif len(selection) == 1: + # One selected item. Use group if its a LayerSet (group), else + # create a new group. + if selection[0].LayerType == group_constant: + groups.append(selection[0]) + else: + layers.append(selection[0]) + elif len(selection) == 0: + # No selection creates an empty group. + create_group = True + else: + create_group = True + + if create_group: + group = photoshop.app().ActiveDocument.LayerSets.Add() + group.Name = self.name + groups.append(group) + + for layer in layers: + photoshop.select_layers([layer]) + group = photoshop.group_selected_layers() + group.Name = layer.Name + groups.append(group) + + for group in groups: + photoshop.imprint(group, self.data) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py new file mode 100644 index 0000000000..22fa85463c --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -0,0 +1,47 @@ +import os + +import pyblish.api +import pype.api +from avalon import photoshop + + +class ValidateNamingRepair(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + instance[0].Name = instance.data["name"].replace(" ", "_") + + return True + + +class ValidateNaming(pyblish.api.InstancePlugin): + """Validate the instance name. + + Spaces in names are not allowed. Will be replace with underscores. + """ + + label = "Validate Naming" + hosts = ["photoshop"] + order = pype.api.ValidateContentsOrder + families = ["image"] + actions = [ValidateNamingRepair] + + def process(self, instance): + msg = "Name \"{}\" is not allowed.".format(instance.data["name"]) + assert " " not in instance.data["name"], msg From f87da7168c77ed8d151c80fd8b3cbd9cc3539ccf Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Jun 2020 15:29:06 +0100 Subject: [PATCH 48/50] Hound --- pype/plugins/photoshop/publish/validate_naming.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pype/plugins/photoshop/publish/validate_naming.py b/pype/plugins/photoshop/publish/validate_naming.py index 22fa85463c..1d85ea99a0 100644 --- a/pype/plugins/photoshop/publish/validate_naming.py +++ b/pype/plugins/photoshop/publish/validate_naming.py @@ -1,8 +1,5 @@ -import os - import pyblish.api import pype.api -from avalon import photoshop class ValidateNamingRepair(pyblish.api.Action): From 8ece0dc8fe516712af04fd603bdea739f7e652a5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 23 Jun 2020 11:50:11 +0200 Subject: [PATCH 49/50] changed check of "error" of result after action --- pype/tools/pyblish_pype/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/tools/pyblish_pype/window.py b/pype/tools/pyblish_pype/window.py index 2967488572..3c7808496c 100644 --- a/pype/tools/pyblish_pype/window.py +++ b/pype/tools/pyblish_pype/window.py @@ -984,7 +984,7 @@ class Window(QtWidgets.QDialog): self._suspend_logs ) - if "error" in result: + if result.get("error"): action_state |= PluginActionStates.HasFailed plugin_item.setData(action_state, Roles.PluginActionProgressRole) From a375a6e29bd1c679bb48b008dbd392c069539b8d Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Tue, 23 Jun 2020 15:49:24 +0200 Subject: [PATCH 50/50] bump version --- pype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/version.py b/pype/version.py index 334087f851..1c622223ba 100644 --- a/pype/version.py +++ b/pype/version.py @@ -1 +1 @@ -__version__ = "2.9.1" +__version__ = "2.10.0"