From 4ae8f277b99298018b999c1539e051fd82eb4de7 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 29 May 2024 22:29:13 +0300 Subject: [PATCH 001/546] add 'pyblish_debug_stepper' tool --- .../pyblish_debug_stepper.py | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py new file mode 100644 index 0000000000..e6cf3c69d6 --- /dev/null +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -0,0 +1,269 @@ +""" +Brought from https://gist.github.com/BigRoy/1972822065e38f8fae7521078e44eca2 +Code Credits: [BigRoy](https://github.com/BigRoy) + +Requirement: + This tool requires some modification in ayon-core. + Add the following two lines in sa similar fashion to this commit + https://github.com/ynput/OpenPype/commit/6a0ce21aa1f8cb17452fe066aa15134d22fda440 + i.e. Add them just after + https://github.com/ynput/ayon-core/blob/8366d2e8b4003a252b8da822f7e38c6db08292b4/client/ayon_core/tools/publisher/control.py#L2483-L2487 + + ``` + result["context"] = self._publish_context + pyblish.api.emit("pluginProcessedCustom", result=result) + ``` + + This modification should be temporary till the following PR get merged and released. + https://github.com/pyblish/pyblish-base/pull/401 + +How it works: + It registers a callback function `on_plugin_processed` + when event `pluginProcessedCustom` is emitted. + The logic of this function is roughly: + 1. Pauses the publishing. + 2. Collects some info about the plugin. + 3. Shows that info to the tool's window. + 4. Continues publishing on clicking `step` button. + +How to use it: + 1. Launch the tool from AYON experimental tools window. + 2. Launch the publisher tool and click validate. + 3. Click Step to run plugins one by one. + +Note: + It won't work when triggering validation from code as our custom event lives inside ayon-core. + But, It should work when the mentioned PR above (#401) get merged and released. + +""" + +import copy +import json +from qtpy import QtWidgets, QtCore, QtGui + +import pyblish.api +from ayon_core import style + +TAB = 4* " " +HEADER_SIZE = "15px" + +KEY_COLOR = QtGui.QColor("#ffffff") +NEW_KEY_COLOR = QtGui.QColor("#00ff00") +VALUE_TYPE_COLOR = QtGui.QColor("#ffbbbb") +NEW_VALUE_TYPE_COLOR = QtGui.QColor("#ff4444") +VALUE_COLOR = QtGui.QColor("#777799") +NEW_VALUE_COLOR = QtGui.QColor("#DDDDCC") +CHANGED_VALUE_COLOR = QtGui.QColor("#CCFFCC") + +MAX_VALUE_STR_LEN = 100 + + +def failsafe_deepcopy(data): + """Allow skipping the deepcopy for unsupported types""" + try: + return copy.deepcopy(data) + except TypeError: + if isinstance(data, dict): + return { + key: failsafe_deepcopy(value) + for key, value in data.items() + } + elif isinstance(data, list): + return data.copy() + return data + + +class DictChangesModel(QtGui.QStandardItemModel): + # TODO: Replace this with a QAbstractItemModel + def __init__(self, *args, **kwargs): + super(DictChangesModel, self).__init__(*args, **kwargs) + self._data = {} + + columns = ["Key", "Type", "Value"] + self.setColumnCount(len(columns)) + for i, label in enumerate(columns): + self.setHeaderData(i, QtCore.Qt.Horizontal, label) + + def _update_recursive(self, data, parent, previous_data): + for key, value in data.items(): + + # Find existing item or add new row + parent_index = parent.index() + for row in range(self.rowCount(parent_index)): + # Update existing item if it exists + index = self.index(row, 0, parent_index) + if index.data() == key: + item = self.itemFromIndex(index) + type_item = self.itemFromIndex(self.index(row, 1, parent_index)) + value_item = self.itemFromIndex(self.index(row, 2, parent_index)) + break + else: + item = QtGui.QStandardItem(key) + type_item = QtGui.QStandardItem() + value_item = QtGui.QStandardItem() + parent.appendRow([item, type_item, value_item]) + + # Key + key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR + item.setData(key_color, QtCore.Qt.ForegroundRole) + + # Type + type_str = type(value).__name__ + type_color = VALUE_TYPE_COLOR + if key in previous_data and type(previous_data[key]).__name__ != type_str: + type_color = NEW_VALUE_TYPE_COLOR + + type_item.setText(type_str) + type_item.setData(type_color, QtCore.Qt.ForegroundRole) + + # Value + value_changed = False + if key not in previous_data or previous_data[key] != value: + value_changed = True + value_color = NEW_VALUE_COLOR if value_changed else VALUE_COLOR + + value_item.setData(value_color, QtCore.Qt.ForegroundRole) + if value_changed: + value_str = str(value) + if len(value_str) > MAX_VALUE_STR_LEN: + value_str = value_str[:MAX_VALUE_STR_LEN] + "..." + value_item.setText(value_str) + # Preferably this is deferred to only when the data gets requested + # since this formatting can be slow for very large data sets like + # project settings and system settings + # This will also be MUCH MUCH faster if we don't clear the items on each update + # but only updated/add/remove changed items so that this also runs much less often + value_item.setData(json.dumps(value, default=str, indent=4), QtCore.Qt.ToolTipRole) + + + if isinstance(value, dict): + previous_value = previous_data.get(key, {}) + if previous_data.get(key) != value: + # Update children if the value is not the same as before + self._update_recursive(value, parent=item, previous_data=previous_value) + else: + # TODO: Ensure all children are updated to be not marked as 'changed' + # in the most optimal way possible + self._update_recursive(value, parent=item, previous_data=previous_value) + + self._data = data + + def update(self, data): + parent = self.invisibleRootItem() + + data = failsafe_deepcopy(data) + previous_data = self._data + self._update_recursive(data, parent, previous_data) + self._data = data # store previous data for next update + + +class DebugUI(QtWidgets.QDialog): + + def __init__(self, parent=None): + super(DebugUI, self).__init__(parent=parent) + self.setStyleSheet(style.load_stylesheet()) + + self._set_window_title() + self.setWindowFlags( + QtCore.Qt.Window + | QtCore.Qt.CustomizeWindowHint + | QtCore.Qt.WindowTitleHint + | QtCore.Qt.WindowMinimizeButtonHint + | QtCore.Qt.WindowCloseButtonHint + | QtCore.Qt.WindowStaysOnTopHint + ) + + layout = QtWidgets.QVBoxLayout(self) + text_edit = QtWidgets.QTextEdit() + text_edit.setFixedHeight(65) + font = QtGui.QFont("NONEXISTENTFONT") + font.setStyleHint(font.TypeWriter) + text_edit.setFont(font) + text_edit.setLineWrapMode(text_edit.NoWrap) + + step = QtWidgets.QPushButton("Step") + step.setEnabled(False) + + model = DictChangesModel() + proxy = QtCore.QSortFilterProxyModel() + proxy.setSourceModel(model) + view = QtWidgets.QTreeView() + view.setModel(proxy) + view.setSortingEnabled(True) + + layout.addWidget(text_edit) + layout.addWidget(view) + layout.addWidget(step) + + step.clicked.connect(self.on_step) + + self._pause = False + self.model = model + self.proxy = proxy + self.view = view + self.text = text_edit + self.step = step + self.resize(700, 500) + + self._previous_data = {} + + + + def _set_window_title(self, plugin=None): + title = "Pyblish Debug Stepper" + if plugin is not None: + plugin_label = plugin.label or plugin.__name__ + title += f" | {plugin_label}" + self.setWindowTitle(title) + + def pause(self, state): + self._pause = state + self.step.setEnabled(state) + + def on_step(self): + self.pause(False) + + def showEvent(self, event): + print("Registering callback..") + pyblish.api.register_callback("pluginProcessedCustom", # "pluginProcessed" + self.on_plugin_processed) + + def hideEvent(self, event): + self.pause(False) + print("Deregistering callback..") + pyblish.api.deregister_callback("pluginProcessedCustom", # "pluginProcessed" + self.on_plugin_processed) + + def on_plugin_processed(self, result): + self.pause(True) + + self._set_window_title(plugin=result["plugin"]) + + print(10*"<" ,result["plugin"].__name__, 10*">") + + plugin_order = result["plugin"].order + plugin_name = result["plugin"].__name__ + duration = result['duration'] + plugin_instance = result["instance"] + context = result["context"] + + msg = "" + msg += f"Order: {plugin_order}
" + msg += f"Plugin: {plugin_name}" + if plugin_instance is not None: + msg += f" -> instance: {plugin_instance}" + msg += "
" + msg += f"Duration: {duration} ms
" + self.text.setHtml(msg) + + data = { + "context": context.data + } + for instance in context: + data[instance.name] = instance.data + self.model.update(data) + + app = QtWidgets.QApplication.instance() + while self._pause: + # Allow user interaction with the UI + app.processEvents() From 3de28f870d54415fc0d14e454f770772cf11d554 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 24 Jun 2024 23:12:23 +0300 Subject: [PATCH 002/546] Add DebugExprimentalTool to ExperimentalTools --- .../tools/experimental_tools/tools_def.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/ayon_core/tools/experimental_tools/tools_def.py b/client/ayon_core/tools/experimental_tools/tools_def.py index 7def3551de..e3acfa72ab 100644 --- a/client/ayon_core/tools/experimental_tools/tools_def.py +++ b/client/ayon_core/tools/experimental_tools/tools_def.py @@ -1,4 +1,5 @@ import os +from .pyblish_debug_stepper import DebugUI # Constant key under which local settings are stored LOCAL_EXPERIMENTAL_KEY = "experimental_tools" @@ -95,6 +96,12 @@ class ExperimentalTools: "hiero", "resolve", ] + ), + ExperimentalHostTool( + "DebugExprimentalTool", + "Pyblish Debug Stepper", + "Debug Pyblish plugins step by step.", + self._show_pyblish_debugger, ) ] @@ -162,6 +169,13 @@ class ExperimentalTools: local_settings.get(LOCAL_EXPERIMENTAL_KEY) ) or {} + # Enable the following tools by default. + # Because they will always be disabled due + # to the fact their settings don't exist. + experimental_settings.update({ + "DebugExprimentalTool": True, + }) + for identifier, eperimental_tool in self.tools_by_identifier.items(): enabled = experimental_settings.get(identifier, False) eperimental_tool.set_enabled(enabled) @@ -175,3 +189,7 @@ class ExperimentalTools: ) self._publisher_tool.show() + + def _show_pyblish_debugger(self): + window = DebugUI(parent=self._parent_widget) + window.show() From 36caeba8dbd5ed3da3f736bebed52330f90b12a9 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 24 Jun 2024 23:26:34 +0300 Subject: [PATCH 003/546] Update pyblish debug stepper docstring --- .../pyblish_debug_stepper.py | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index e6cf3c69d6..51b7ba2c06 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -3,37 +3,25 @@ Brought from https://gist.github.com/BigRoy/1972822065e38f8fae7521078e44eca2 Code Credits: [BigRoy](https://github.com/BigRoy) Requirement: - This tool requires some modification in ayon-core. - Add the following two lines in sa similar fashion to this commit - https://github.com/ynput/OpenPype/commit/6a0ce21aa1f8cb17452fe066aa15134d22fda440 - i.e. Add them just after - https://github.com/ynput/ayon-core/blob/8366d2e8b4003a252b8da822f7e38c6db08292b4/client/ayon_core/tools/publisher/control.py#L2483-L2487 - - ``` - result["context"] = self._publish_context - pyblish.api.emit("pluginProcessedCustom", result=result) - ``` - - This modification should be temporary till the following PR get merged and released. - https://github.com/pyblish/pyblish-base/pull/401 + It requires pyblish version >= 1.8.12 How it works: - It registers a callback function `on_plugin_processed` - when event `pluginProcessedCustom` is emitted. - The logic of this function is roughly: - 1. Pauses the publishing. - 2. Collects some info about the plugin. - 3. Shows that info to the tool's window. - 4. Continues publishing on clicking `step` button. + This tool makes use of pyblish event `pluginProcessed` to: + 1. Pause the publishing. + 2. Collect some info about the plugin. + 3. Show that info to the tool's window. + 4. Continue publishing on clicking `step` button. How to use it: 1. Launch the tool from AYON experimental tools window. 2. Launch the publisher tool and click validate. 3. Click Step to run plugins one by one. -Note: - It won't work when triggering validation from code as our custom event lives inside ayon-core. - But, It should work when the mentioned PR above (#401) get merged and released. +Note : + Pyblish debugger also works when triggering the validation or + publishing from code. + Here's an example about validating from code: + https://github.com/MustafaJafar/ayon-recipes/blob/main/validate_from_code.py """ @@ -225,13 +213,13 @@ class DebugUI(QtWidgets.QDialog): def showEvent(self, event): print("Registering callback..") - pyblish.api.register_callback("pluginProcessedCustom", # "pluginProcessed" + pyblish.api.register_callback("pluginProcessed", self.on_plugin_processed) def hideEvent(self, event): self.pause(False) print("Deregistering callback..") - pyblish.api.deregister_callback("pluginProcessedCustom", # "pluginProcessed" + pyblish.api.deregister_callback("pluginProcessed", self.on_plugin_processed) def on_plugin_processed(self, result): From f5006273ae69f6cb6404522488ae3141ed376200 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 5 Jul 2024 19:33:23 +0300 Subject: [PATCH 004/546] fix typo --- client/ayon_core/tools/experimental_tools/tools_def.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/tools_def.py b/client/ayon_core/tools/experimental_tools/tools_def.py index e3acfa72ab..30e5211b41 100644 --- a/client/ayon_core/tools/experimental_tools/tools_def.py +++ b/client/ayon_core/tools/experimental_tools/tools_def.py @@ -98,7 +98,7 @@ class ExperimentalTools: ] ), ExperimentalHostTool( - "DebugExprimentalTool", + "pyblish_debug_stepper", "Pyblish Debug Stepper", "Debug Pyblish plugins step by step.", self._show_pyblish_debugger, @@ -173,12 +173,12 @@ class ExperimentalTools: # Because they will always be disabled due # to the fact their settings don't exist. experimental_settings.update({ - "DebugExprimentalTool": True, + "pyblish_debug_stepper": True, }) - for identifier, eperimental_tool in self.tools_by_identifier.items(): + for identifier, experimental_tool in self.tools_by_identifier.items(): enabled = experimental_settings.get(identifier, False) - eperimental_tool.set_enabled(enabled) + experimental_tool.set_enabled(enabled) def _show_publisher(self): if self._publisher_tool is None: From 640c24d9b70665d1ecec6cf293759c213eaf4271 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:33:39 +0200 Subject: [PATCH 005/546] add 'parents' to folder template data --- client/ayon_core/pipeline/template_data.py | 13 +++++++------ .../publish/collect_anatomy_instance_data.py | 5 ++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index d5f06d6a59..2c5346f14b 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -87,14 +87,14 @@ def get_folder_template_data(folder_entity, project_name): """ path = folder_entity["path"] - hierarchy_parts = path.split("/") + parents = path.split("/") # Remove empty string from the beginning - hierarchy_parts.pop(0) + parents.pop(0) # Remove last part which is folder name - folder_name = hierarchy_parts.pop(-1) - hierarchy = "/".join(hierarchy_parts) - if hierarchy_parts: - parent_name = hierarchy_parts[-1] + folder_name = parents.pop(-1) + hierarchy = "/".join(parents) + if parents: + parent_name = parents[-1] else: parent_name = project_name @@ -103,6 +103,7 @@ def get_folder_template_data(folder_entity, project_name): "name": folder_name, "type": folder_entity["folderType"], "path": path, + "parents": parents, }, "asset": folder_name, "hierarchy": hierarchy, diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index b6636696c1..6eeca6ad29 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -407,8 +407,10 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): anatomy_data["hierarchy"] = hierarchy parent_name = project_entity["name"] + parents = [] if hierarchy: - parent_name = hierarchy.split("/")[-1] + parents = hierarchy.split("/") + parent_name = parents[-1] folder_name = instance.data["folderPath"].split("/")[-1] anatomy_data.update({ @@ -422,6 +424,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # Using 'Shot' is current default behavior of editorial # (or 'newHierarchyIntegration') publishing. "type": "Shot", + "parents": parents, }, }) From c08d0baa88839020caa7e9372fb286f100d56fc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:57:55 +0200 Subject: [PATCH 006/546] simplify split --- client/ayon_core/pipeline/template_data.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index 2c5346f14b..c7aa46fd62 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -87,9 +87,8 @@ def get_folder_template_data(folder_entity, project_name): """ path = folder_entity["path"] - parents = path.split("/") - # Remove empty string from the beginning - parents.pop(0) + # Remove empty string from the beginning and split by '/' + parents = path.lstrip("/").split("/") # Remove last part which is folder name folder_name = parents.pop(-1) hierarchy = "/".join(parents) From d1bae6d167e086333bd70ad637d52216981c8fac Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:29:48 +0200 Subject: [PATCH 007/546] don't store 'attr_plugins' --- client/ayon_core/pipeline/create/structures.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 4f7caa6e11..45839ddaf5 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -252,9 +252,6 @@ class PublishAttributes: self.parent = parent self._origin_data = copy.deepcopy(origin_data) - attr_plugins = attr_plugins or [] - self.attr_plugins = attr_plugins - self._data = copy.deepcopy(origin_data) self._plugin_names_order = [] self._missing_plugins = [] @@ -325,10 +322,9 @@ class PublishAttributes: def set_publish_plugins(self, attr_plugins): """Set publish plugins attribute definitions.""" - + attr_plugins = attr_plugins or [] self._plugin_names_order = [] self._missing_plugins = [] - self.attr_plugins = attr_plugins or [] origin_data = self._origin_data data = self._data From c337f2cc281c1d91967b9aa14fe1b19d1ea075fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 29 Aug 2024 16:50:47 +0200 Subject: [PATCH 008/546] added 2 new methods to be able to receive attribute definitions per instance --- .../pipeline/publish/publish_plugins.py | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 6b1984d92b..3593e781b4 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -1,6 +1,7 @@ import inspect from abc import ABCMeta import pyblish.api +import pyblish.logic from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin from ayon_core.lib import BoolDef @@ -114,10 +115,53 @@ class AYONPyblishPluginMixin: """Publish attribute definitions. Attributes available for all families in plugin's `families` attribute. - Returns: - list: Attribute definitions for plugin. - """ + Returns: + list[AbstractAttrDef]: Attribute definitions for plugin. + + """ + return [] + + @classmethod + def get_attribute_defs_for_context(cls, create_context): + """Publish attribute definitions for context. + + Attributes available for all families in plugin's `families` attribute. + + Args: + create_context (CreateContext): Create context. + + Returns: + list[AbstractAttrDef]: Attribute definitions for plugin. + + """ + if cls.__instanceEnabled__: + return [] + return cls.get_attribute_defs() + + @classmethod + def get_attribute_defs_for_instance(cls, create_context, instance): + """Publish attribute definitions for an instance. + + Attributes available for all families in plugin's `families` attribute. + + + Args: + create_context (CreateContext): Create context. + instance (CreatedInstance): Instance for which attributes are + collected. + + Returns: + list[AbstractAttrDef]: Attribute definitions for plugin. + + """ + if not cls.__instanceEnabled__: + return [] + + for _ in pyblish.logic.plugins_by_families( + [cls], [instance.product_type] + ): + return cls.get_attribute_defs() return [] @classmethod From 738cc82f2617bd1d336926535d00ac7c0e3ee03e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:48:12 +0200 Subject: [PATCH 009/546] added helper methods to create plugin --- .../pipeline/create/creator_plugins.py | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 61c10ee736..3b90b11a51 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import copy import collections -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, Dict, Any from abc import ABC, abstractmethod @@ -19,11 +19,12 @@ from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name from .utils import get_next_versions_for_instances from .legacy_create import LegacyCreator +from .structures import CreatedInstance if TYPE_CHECKING: from ayon_core.lib import AbstractAttrDef # Avoid cyclic imports - from .context import CreateContext, CreatedInstance, UpdateData # noqa: F401 + from .context import CreateContext, UpdateData # noqa: F401 class ProductConvertorPlugin(ABC): @@ -362,6 +363,18 @@ class BaseCreator(ABC): self._log = Logger.get_logger(self.__class__.__name__) return self._log + def _create_instance( + self, product_type: str, product_name: str, data: Dict[str, Any] + ) -> CreatedInstance: + instance = CreatedInstance( + product_type, + product_name, + data, + creator=self, + ) + self._add_instance_to_context(instance) + return instance + def _add_instance_to_context(self, instance): """Helper method to add instance to create context. @@ -551,6 +564,16 @@ class BaseCreator(ABC): return self.instance_attr_defs + def get_attr_defs_for_instance(self, instance): + """Get attribute definitions for an instance. + + Args: + instance (CreatedInstance): Instance for which to get + attribute definitions. + + """ + return self.get_instance_attr_defs() + @property def collection_shared_data(self): """Access to shared data that can be used during creator's collection. From cb3df926a9f31373c9728135c277e2a59aea5182 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 29 Aug 2024 18:59:56 +0200 Subject: [PATCH 010/546] attribute definitions are defined per instance --- client/ayon_core/pipeline/create/context.py | 88 +++++++-------- .../ayon_core/pipeline/create/structures.py | 105 +++++++----------- .../pipeline/publish/publish_plugins.py | 20 ++-- 3 files changed, 85 insertions(+), 128 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3f067427fa..3387a5a5fa 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -6,7 +6,7 @@ import traceback import collections import inspect from contextlib import contextmanager -from typing import Optional +from typing import Optional, Dict, Any, Callable import pyblish.logic import pyblish.api @@ -171,7 +171,6 @@ class CreateContext: self.publish_plugins_mismatch_targets = [] self.publish_plugins = [] self.plugins_with_defs = [] - self._attr_plugins_by_product_type = {} # Helpers for validating context of collected instances # - they can be validation for multiple instances at one time @@ -564,9 +563,6 @@ class CreateContext: publish_plugins_discover ) - # Reset publish plugins - self._attr_plugins_by_product_type = {} - discover_result = DiscoverResult(pyblish.api.Plugin) plugins_with_defs = [] plugins_by_targets = [] @@ -694,11 +690,29 @@ class CreateContext: publish_attributes = original_data.get("publish_attributes") or {} - attr_plugins = self._get_publish_plugins_with_attr_for_context() self._publish_attributes = PublishAttributes( - self, publish_attributes, attr_plugins + self, publish_attributes ) + for plugin in self.plugins_with_defs: + if is_func_signature_supported( + plugin.convert_attribute_values, self, None + ): + plugin.convert_attribute_values(self, None) + + elif not plugin.__instanceEnabled__: + output = plugin.convert_attribute_values(publish_attributes) + if output: + publish_attributes.update(output) + + for plugin in self.plugins_with_defs: + attr_defs = plugin.get_attribute_defs_for_context(self) + if not attr_defs: + continue + self._publish_attributes.set_publish_plugin_attr_defs( + plugin.__name__, attr_defs + ) + def context_data_to_store(self): """Data that should be stored by host function. @@ -734,11 +748,25 @@ class CreateContext: return self._instances_by_id[instance.id] = instance + + publish_attributes = instance.publish_attributes # Prepare publish plugin attributes and set it on instance - attr_plugins = self._get_publish_plugins_with_attr_for_product_type( - instance.product_type - ) - instance.set_publish_plugins(attr_plugins) + for plugin in self.plugins_with_defs: + if is_func_signature_supported( + plugin.convert_attribute_values, self, instance + ): + plugin.convert_attribute_values(self, instance) + + elif plugin.__instanceEnabled__: + output = plugin.convert_attribute_values(publish_attributes) + if output: + publish_attributes.update(output) + + for plugin in self.plugins_with_defs: + attr_defs = plugin.get_attribute_defs_for_instance(self, instance) + if not attr_defs: + continue + instance.set_publish_plugin_attr_defs(plugin.__name__, attr_defs) # Add instance to be validated inside 'bulk_instances_collection' # context manager if is inside bulk @@ -1309,44 +1337,6 @@ class CreateContext: if failed_info: raise CreatorsRemoveFailed(failed_info) - def _get_publish_plugins_with_attr_for_product_type(self, product_type): - """Publish plugin attributes for passed product type. - - Attribute definitions for specific product type are cached. - - Args: - product_type(str): Instance product type for which should be - attribute definitions returned. - """ - - if product_type not in self._attr_plugins_by_product_type: - import pyblish.logic - - filtered_plugins = pyblish.logic.plugins_by_families( - self.plugins_with_defs, [product_type] - ) - plugins = [] - for plugin in filtered_plugins: - if plugin.__instanceEnabled__: - plugins.append(plugin) - self._attr_plugins_by_product_type[product_type] = plugins - - return self._attr_plugins_by_product_type[product_type] - - def _get_publish_plugins_with_attr_for_context(self): - """Publish plugins attributes for Context plugins. - - Returns: - List[pyblish.api.Plugin]: Publish plugins that have attribute - definitions for context. - """ - - plugins = [] - for plugin in self.plugins_with_defs: - if not plugin.__instanceEnabled__: - plugins.append(plugin) - return plugins - @property def collection_shared_data(self): """Access to shared data that can be used during creator's collection. diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 45839ddaf5..81b0ce7d6d 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -4,6 +4,7 @@ from uuid import uuid4 from ayon_core.lib.attribute_definitions import ( UnknownDef, + UIDef, serialize_attr_defs, deserialize_attr_defs, ) @@ -248,15 +249,11 @@ class PublishAttributes: plugins that may have defined attribute definitions. """ - def __init__(self, parent, origin_data, attr_plugins=None): + def __init__(self, parent, origin_data): self.parent = parent self._origin_data = copy.deepcopy(origin_data) self._data = copy.deepcopy(origin_data) - self._plugin_names_order = [] - self._missing_plugins = [] - - self.set_publish_plugins(attr_plugins) def __getitem__(self, key): return self._data[key] @@ -287,10 +284,9 @@ class PublishAttributes: if key not in self._data: return default - if key in self._missing_plugins: - self._missing_plugins.remove(key) - removed_item = self._data.pop(key) - return removed_item.data_to_store() + value = self._data[key] + if not isinstance(value, AttributeValues): + return self._data.pop(key) value_item = self._data[key] # Prepare value to return @@ -299,12 +295,6 @@ class PublishAttributes: value_item.reset_values() return output - def plugin_names_order(self): - """Plugin names order by their 'order' attribute.""" - - for name in self._plugin_names_order: - yield name - def mark_as_stored(self): self._origin_data = copy.deepcopy(self.data_to_store()) @@ -320,40 +310,29 @@ class PublishAttributes: def origin_data(self): return copy.deepcopy(self._origin_data) - def set_publish_plugins(self, attr_plugins): - """Set publish plugins attribute definitions.""" - attr_plugins = attr_plugins or [] - self._plugin_names_order = [] - self._missing_plugins = [] + def set_publish_plugin_attr_defs(self, plugin_name, attr_defs): + """Set attribute definitions for plugin. - origin_data = self._origin_data - data = self._data - self._data = {} - added_keys = set() - for plugin in attr_plugins: - output = plugin.convert_attribute_values(data) - if output is not None: - data = output - attr_defs = plugin.get_attribute_defs() - if not attr_defs: + Args: + plugin_name(str): Name of plugin. + attr_defs(Optional[List[AbstractAttrDef]]): Attribute definitions. + + """ + # TODO what if 'attr_defs' is 'None'? + value = self._data.get(plugin_name) + if value is None: + value = {} + + for attr_def in attr_defs: + if isinstance(attr_def, (UIDef, UnknownDef)): continue + key = attr_def.key + if key in value: + value[key] = attr_def.convert_value(value[key]) - key = plugin.__name__ - added_keys.add(key) - self._plugin_names_order.append(key) - - value = data.get(key) or {} - orig_value = copy.deepcopy(origin_data.get(key) or {}) - self._data[key] = PublishAttributeValues( - self, attr_defs, value, orig_value - ) - - for key, value in data.items(): - if key not in added_keys: - self._missing_plugins.append(key) - self._data[key] = PublishAttributeValues( - self, [], value, value - ) + self._data[plugin_name] = PublishAttributeValues( + self, attr_defs, value, value + ) def serialize_attributes(self): return { @@ -361,14 +340,9 @@ class PublishAttributes: plugin_name: attrs_value.get_serialized_attr_defs() for plugin_name, attrs_value in self._data.items() }, - "plugin_names_order": self._plugin_names_order, - "missing_plugins": self._missing_plugins } def deserialize_attributes(self, data): - self._plugin_names_order = data["plugin_names_order"] - self._missing_plugins = data["missing_plugins"] - attr_defs = deserialize_attr_defs(data["attr_defs"]) origin_data = self._origin_data @@ -386,10 +360,7 @@ class PublishAttributes: for key, value in data.items(): if key not in added_keys: - self._missing_plugins.append(key) - self._data[key] = PublishAttributeValues( - self, [], value, value - ) + self._data[key] = value class CreatedInstance: @@ -445,7 +416,6 @@ class CreatedInstance: creator_identifier = creator.identifier group_label = creator.get_group_label() creator_label = creator.label - creator_attr_defs = creator.get_instance_attr_defs() self._creator_label = creator_label self._group_label = group_label or creator_identifier @@ -505,6 +475,9 @@ class CreatedInstance: # {key: value} creator_values = copy.deepcopy(orig_creator_attributes) + if creator is not None: + creator_attr_defs = creator.get_attr_defs_for_instance(self) + self._data["creator_attributes"] = CreatorAttributeValues( self, list(creator_attr_defs), @@ -514,9 +487,8 @@ class CreatedInstance: # Stored publish specific attribute values # {: {key: value}} - # - must be set using 'set_publish_plugins' self._data["publish_attributes"] = PublishAttributes( - self, orig_publish_attributes, None + self, orig_publish_attributes ) if data: self._data.update(data) @@ -745,18 +717,17 @@ class CreatedInstance: product_type, product_name, instance_data, creator ) - def set_publish_plugins(self, attr_plugins): - """Set publish plugins with attribute definitions. - - This method should be called only from 'CreateContext'. + def set_publish_plugin_attr_defs(self, plugin_name, attr_defs): + """Set attribute definitions for publish plugin. Args: - attr_plugins (List[pyblish.api.Plugin]): Pyblish plugins which - inherit from 'AYONPyblishPluginMixin' and may contain - attribute definitions. - """ + plugin_name(str): Name of publish plugin. + attr_defs(List[AbstractAttrDef]): Attribute definitions. - self.publish_attributes.set_publish_plugins(attr_plugins) + """ + self.publish_attributes.set_publish_plugin_attr_defs( + plugin_name, attr_defs + ) def add_members(self, members): """Currently unused method.""" diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 3593e781b4..6ea2cb1efa 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -165,20 +165,16 @@ class AYONPyblishPluginMixin: return [] @classmethod - def convert_attribute_values(cls, attribute_values): - if cls.__name__ not in attribute_values: - return attribute_values + def convert_attribute_values(cls, create_context, instance): + """Convert attribute values for instance. - plugin_values = attribute_values[cls.__name__] + Args: + create_context (CreateContext): Create context. + instance (CreatedInstance): Instance for which attributes are + converted. - attr_defs = cls.get_attribute_defs() - for attr_def in attr_defs: - key = attr_def.key - if key in plugin_values: - plugin_values[key] = attr_def.convert_value( - plugin_values[key] - ) - return attribute_values + """ + return @staticmethod def get_attr_values_from_data_for_plugin(plugin, data): From 35d727747787d6f67bc73fb26bb2563e0fb5c969 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:43:03 +0200 Subject: [PATCH 011/546] removed unused imports --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3387a5a5fa..81e35a1c11 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -6,7 +6,7 @@ import traceback import collections import inspect from contextlib import contextmanager -from typing import Optional, Dict, Any, Callable +from typing import Optional import pyblish.logic import pyblish.api From d970a1cf20d5440301c48203efa9e131e0c39bc9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:25:37 +0200 Subject: [PATCH 012/546] remove line --- client/ayon_core/pipeline/publish/publish_plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 6ea2cb1efa..147b1d3a6d 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -145,7 +145,6 @@ class AYONPyblishPluginMixin: Attributes available for all families in plugin's `families` attribute. - Args: create_context (CreateContext): Create context. instance (CreatedInstance): Instance for which attributes are From f03983dfdac916f1d5b4d1c74a4e63ba7e905e92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:50:09 +0200 Subject: [PATCH 013/546] use 'get_attr_defs_for_instance' after creation --- client/ayon_core/pipeline/create/structures.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 81b0ce7d6d..c725a98e51 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -475,12 +475,13 @@ class CreatedInstance: # {key: value} creator_values = copy.deepcopy(orig_creator_attributes) - if creator is not None: - creator_attr_defs = creator.get_attr_defs_for_instance(self) - + if creator_attr_defs is None: + _creator_attr_defs = [] + else: + _creator_attr_defs = list(creator_attr_defs) self._data["creator_attributes"] = CreatorAttributeValues( self, - list(creator_attr_defs), + _creator_attr_defs, creator_values, orig_creator_attributes ) @@ -499,6 +500,12 @@ class CreatedInstance: self._folder_is_valid = self.has_set_folder self._task_is_valid = self.has_set_task + if creator is not None: + creator_attr_defs = creator.get_attr_defs_for_instance(self) + self.update_create_attr_defs( + creator_attr_defs, creator_values + ) + def __str__(self): return ( " Date: Fri, 6 Sep 2024 18:51:25 +0200 Subject: [PATCH 014/546] implemented 'update_create_attr_defs' --- client/ayon_core/pipeline/create/structures.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index c725a98e51..7f31a72b0c 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -698,6 +698,17 @@ class CreatedInstance: return output + def update_create_attr_defs(self, attr_defs, value=None): + if value is not None: + value = self._data["creator_attributes"] + origin_data = self._data["creator_attributes"].origin_data + self._data["creator_attributes"] = CreatorAttributeValues( + self, + attr_defs, + value, + origin_data + ) + @classmethod def from_existing(cls, instance_data, creator): """Convert instance data from workfile to CreatedInstance. From b3f39ba41c20ebc10c365a389efa578d810188f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Sep 2024 18:59:28 +0200 Subject: [PATCH 015/546] data to store does not have to have 'AttributeValues' --- client/ayon_core/pipeline/create/structures.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 7f31a72b0c..afefb911cd 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -300,10 +300,12 @@ class PublishAttributes: def data_to_store(self): """Convert attribute values to "data to store".""" - output = {} for key, attr_value in self._data.items(): - output[key] = attr_value.data_to_store() + if isinstance(attr_value, AttributeValues): + output[key] = attr_value.data_to_store() + else: + output[key] = attr_value return output @property From 282d1720ae958bbe8833ac64d6295e658a28b438 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Sep 2024 16:39:09 +0200 Subject: [PATCH 016/546] Add staging directory functions and configurations - Added functions to handle custom staging directories - Updated imports and removed deprecated code - Created a new module for staging directory handling --- client/ayon_core/pipeline/__init__.py | 27 +-- .../ayon_core/pipeline/publish/constants.py | 1 - client/ayon_core/pipeline/publish/lib.py | 175 ++++++-------- client/ayon_core/pipeline/stagingdir.py | 220 ++++++++++++++++++ client/ayon_core/pipeline/tempdir.py | 90 +++++-- .../publish/collect_custom_staging_dir.py | 76 ------ .../plugins/publish/extract_burnin.py | 11 +- .../publish/extract_color_transcode.py | 15 +- .../plugins/publish/extract_review.py | 7 +- 9 files changed, 396 insertions(+), 226 deletions(-) create mode 100644 client/ayon_core/pipeline/stagingdir.py delete mode 100644 client/ayon_core/plugins/publish/collect_custom_staging_dir.py diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 8fd00ee6b6..d5c3140d37 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -8,6 +8,10 @@ from .constants import ( from .anatomy import Anatomy +from .tempdir import get_temp_dir + +from .stagingdir import get_staging_dir + from .create import ( BaseCreator, Creator, @@ -116,10 +120,12 @@ __all__ = ( "AYON_CONTAINER_ID", "AYON_INSTANCE_ID", "HOST_WORKFILE_EXTENSIONS", - # --- Anatomy --- "Anatomy", - + # --- Temp dir --- + "get_temp_dir", + # --- Staging dir --- + "get_staging_dir", # --- Create --- "BaseCreator", "Creator", @@ -127,42 +133,34 @@ __all__ = ( "HiddenCreator", "CreatedInstance", "CreatorError", - "CreatorError", - # - legacy creation "LegacyCreator", "legacy_create", - "discover_creator_plugins", "discover_legacy_creator_plugins", "register_creator_plugin", "deregister_creator_plugin", "register_creator_plugin_path", "deregister_creator_plugin_path", - # --- Load --- "HeroVersionType", "IncompatibleLoaderError", "LoaderPlugin", "ProductLoaderPlugin", - "discover_loader_plugins", "register_loader_plugin", "deregister_loader_plugin_path", "register_loader_plugin_path", "deregister_loader_plugin", - "load_container", "remove_container", "update_container", "switch_container", - "loaders_from_representation", "get_representation_path", "get_representation_context", "get_repres_contexts", - # --- Publish --- "PublishValidationError", "PublishXmlValidationError", @@ -170,50 +168,41 @@ __all__ = ( "AYONPyblishPluginMixin", "OpenPypePyblishPluginMixin", "OptionalPyblishPluginMixin", - # --- Actions --- "LauncherAction", "InventoryAction", - "discover_launcher_actions", "register_launcher_action", "register_launcher_action_path", - "discover_inventory_actions", "register_inventory_action", "register_inventory_action_path", "deregister_inventory_action", "deregister_inventory_action_path", - # --- Process context --- "install_ayon_plugins", "install_openpype_plugins", "install_host", "uninstall_host", "is_installed", - "register_root", "registered_root", - "register_host", "registered_host", "deregister_host", "get_process_id", - "get_global_context", "get_current_context", "get_current_host_name", "get_current_project_name", "get_current_folder_path", "get_current_task_name", - # Workfile templates "discover_workfile_build_plugins", "register_workfile_build_plugin", "deregister_workfile_build_plugin", "register_workfile_build_plugin_path", "deregister_workfile_build_plugin_path", - # Backwards compatible function names "install", "uninstall", diff --git a/client/ayon_core/pipeline/publish/constants.py b/client/ayon_core/pipeline/publish/constants.py index 38f5ffef3f..5240628365 100644 --- a/client/ayon_core/pipeline/publish/constants.py +++ b/client/ayon_core/pipeline/publish/constants.py @@ -8,4 +8,3 @@ ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 DEFAULT_PUBLISH_TEMPLATE = "default" DEFAULT_HERO_PUBLISH_TEMPLATE = "default" -TRANSIENT_DIR_TEMPLATE = "default" diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 8b82622e4c..9cfcd3f71a 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -2,7 +2,6 @@ import os import sys import inspect import copy -import tempfile import xml.etree.ElementTree from typing import Optional, Union, List @@ -18,15 +17,11 @@ from ayon_core.lib import ( ) from ayon_core.settings import get_project_settings from ayon_core.addon import AddonsManager -from ayon_core.pipeline import ( - tempdir, - Anatomy -) +from ayon_core.pipeline import get_staging_dir from ayon_core.pipeline.plugin_discover import DiscoverResult from .constants import ( DEFAULT_PUBLISH_TEMPLATE, DEFAULT_HERO_PUBLISH_TEMPLATE, - TRANSIENT_DIR_TEMPLATE ) @@ -581,58 +576,6 @@ def context_plugin_should_run(plugin, context): return False -def get_instance_staging_dir(instance): - """Unified way how staging dir is stored and created on instances. - - First check if 'stagingDir' is already set in instance data. - In case there already is new tempdir will not be created. - - It also supports `AYON_TMPDIR`, so studio can define own temp - shared repository per project or even per more granular context. - Template formatting is supported also with optional keys. Folder is - created in case it doesn't exists. - - Available anatomy formatting keys: - - root[work | ] - - project[name | code] - - Note: - Staging dir does not have to be necessarily in tempdir so be careful - about its usage. - - Args: - instance (pyblish.lib.Instance): Instance for which we want to get - staging dir. - - Returns: - str: Path to staging dir of instance. - """ - staging_dir = instance.data.get('stagingDir') - if staging_dir: - return staging_dir - - anatomy = instance.context.data.get("anatomy") - - # get customized tempdir path from `AYON_TMPDIR` env var - custom_temp_dir = tempdir.create_custom_tempdir( - anatomy.project_name, anatomy) - - if custom_temp_dir: - staging_dir = os.path.normpath( - tempfile.mkdtemp( - prefix="pyblish_tmp_", - dir=custom_temp_dir - ) - ) - else: - staging_dir = os.path.normpath( - tempfile.mkdtemp(prefix="pyblish_tmp_") - ) - instance.data['stagingDir'] = staging_dir - - return staging_dir - - def get_publish_repre_path(instance, repre, only_published=False): """Get representation path that can be used for integration. @@ -685,6 +628,8 @@ def get_publish_repre_path(instance, repre, only_published=False): return None +# deprecated: backward compatibility only +# TODO: remove in the future def get_custom_staging_dir_info( project_name, host_name, @@ -694,67 +639,85 @@ def get_custom_staging_dir_info( product_name, project_settings=None, anatomy=None, - log=None + log=None, ): - """Checks profiles if context should use special custom dir as staging. + from ayon_core.pipeline.stagingdir import get_staging_dir_config - Args: - project_name (str) - host_name (str) - product_type (str) - task_name (str) - task_type (str) - product_name (str) - project_settings(Dict[str, Any]): Prepared project settings. - anatomy (Dict[str, Any]) - log (Logger) (optional) + tr_data = get_staging_dir_config( + host_name, + project_name, + task_type, + task_name, + product_type, + product_name, + project_settings=project_settings, + anatomy=anatomy, + log=log, + ) + + if not tr_data: + return None, None + + return tr_data["template"], tr_data["persistence"] + + +def get_instance_staging_dir(instance): + """Unified way how staging dir is stored and created on instances. + + First check if 'stagingDir' is already set in instance data. + In case there already is new tempdir will not be created. Returns: - (tuple) - Raises: - ValueError - if misconfigured template should be used + str: Path to staging dir """ - settings = project_settings or get_project_settings(project_name) - custom_staging_dir_profiles = (settings["core"] - ["tools"] - ["publish"] - ["custom_staging_dir_profiles"]) - if not custom_staging_dir_profiles: - return None, None + staging_dir = instance.data.get("stagingDir") - if not log: - log = Logger.get_logger("get_custom_staging_dir_info") + if staging_dir: + return staging_dir - filtering_criteria = { - "hosts": host_name, - "families": product_type, - "task_names": task_name, - "task_types": task_type, - "subsets": product_name - } - profile = filter_profiles(custom_staging_dir_profiles, - filtering_criteria, - logger=log) + anatomy_data = instance.data["anatomyData"] + formatting_data = copy.deepcopy(anatomy_data) - if not profile or not profile["active"]: - return None, None + product_type = instance.data["productType"] + product_name = instance.data["productName"] - if not anatomy: - anatomy = Anatomy(project_name) + # context data based variables + project_entity = instance.context.data["projectEntity"] + folder_entity = instance.context.data["folderEntity"] + task_entity = instance.context.data["taskEntity"] + host_name = instance.context.data["hostName"] + project_settings = instance.context.data["project_settings"] + anatomy = instance.context.data["anatomy"] + current_file = instance.context.data.get("currentFile") - template_name = profile["template_name"] or TRANSIENT_DIR_TEMPLATE + # add current file as workfile name into formatting data + if current_file: + workfile = os.path.basename(current_file) + workfile_name, _ = os.path.splitext(workfile) + formatting_data["workfile_name"] = workfile_name - custom_staging_dir = anatomy.get_template_item( - "staging", template_name, "directory", default=None + dir_data = get_staging_dir( + host_name, + project_entity, + folder_entity, + task_entity, + product_type, + product_name, + anatomy, + project_settings=project_settings, + formatting_data=formatting_data, ) - if custom_staging_dir is None: - raise ValueError(( - "Anatomy of project \"{}\" does not have set" - " \"{}\" template key!" - ).format(project_name, template_name)) - is_persistent = profile["custom_staging_dir_persistent"] - return custom_staging_dir.template, is_persistent + staging_dir_path = dir_data["stagingDir"] + + # TODO: not sure if this is necessary + # path might be already created by get_staging_dir + if not os.path.exists(staging_dir_path): + os.makedirs(staging_dir_path) + + instance.data.update(dir_data) + + return staging_dir_path def get_published_workfile_instance(context): diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py new file mode 100644 index 0000000000..e8fa1c4853 --- /dev/null +++ b/client/ayon_core/pipeline/stagingdir.py @@ -0,0 +1,220 @@ +from ayon_core.lib import Logger, filter_profiles, StringTemplate +from ayon_core.settings import get_project_settings +from .anatomy import Anatomy +from .tempdir import get_temp_dir +from ayon_core.pipeline.template_data import get_template_data + + +STAGING_DIR_TEMPLATES = "staging" + + +def get_staging_dir_config( + host_name, + project_name, + task_type, + task_name, + product_type, + product_name, + project_settings=None, + anatomy=None, + log=None, +): + """Get matching staging dir profile. + + Args: + host_name (str): Name of host. + project_name (str): Name of project. + task_type (str): Type of task. + task_name (str): Name of task. + product_type (str): Type of product. + product_name (str): Name of product. + project_settings(Dict[str, Any]): Prepared project settings. + anatomy (Dict[str, Any]) + log (Optional[logging.Logger]) + + Returns: + Dict or None: Data with directory template and is_persistent or None + Raises: + ValueError - if misconfigured template should be used + """ + settings = project_settings or get_project_settings(project_name) + + staging_dir_profiles = settings["core"]["tools"]["publish"][ + "custom_staging_dir_profiles" + ] + + if not staging_dir_profiles: + return None + + if not log: + log = Logger.get_logger("get_staging_dir_config") + + filtering_criteria = { + "hosts": host_name, + "task_types": task_type, + "task_names": task_name, + "product_types": product_type, + "product_names": product_name, + } + profile = filter_profiles( + staging_dir_profiles, filtering_criteria, logger=log) + + if not profile or not profile["active"]: + return None + + if not anatomy: + anatomy = Anatomy(project_name) + + # get template from template name + template_name = profile["template_name"] + _validate_template_name(project_name, template_name, anatomy) + + template = anatomy.templates[STAGING_DIR_TEMPLATES][template_name] + + if not template: + # template should always be found either from anatomy or from profile + raise ValueError( + "Staging dir profile is misconfigured! " + "No template was found for profile! " + "Check your project settings at: " + "'ayon+settings://core/tools/publish/custom_staging_dir_profiles'" + ) + + data_persistence = profile["custom_staging_dir_persistent"] + + return {"template": template, "persistence": data_persistence} + + +def _validate_template_name(project_name, template_name, anatomy): + """Check that staging dir section with appropriate template exist. + + Raises: + ValueError - if misconfigured template + """ + # TODO: only for backward compatibility of anatomy for older projects + if STAGING_DIR_TEMPLATES not in anatomy.templates: + raise ValueError( + ( + 'Anatomy of project "{}" does not have set' ' "{}" template section!' + ).format(project_name, template_name) + ) + + if template_name not in anatomy.templates[STAGING_DIR_TEMPLATES]: + raise ValueError( + ( + 'Anatomy of project "{}" does not have set' + ' "{}" template key at Staging Dir section!' + ).format(project_name, template_name) + ) + + +def get_staging_dir( + host_name, + project_entity, + folder_entity, + task_entity, + product_type, + product_name, + anatomy, + project_settings=None, + **kwargs +): + """Get staging dir data. + + If `force_temp` is set, staging dir will be created as tempdir. + If `always_get_some_dir` is set, staging dir will be created as tempdir if + no staging dir profile is found. + If `prefix` or `suffix` is not set, default values will be used. + + Arguments: + host_name (str): Name of host. + project_entity (Dict[str, Any]): Project entity. + folder_entity (Dict[str, Any]): Folder entity. + task_entity (Dict[str, Any]): Task entity. + product_type (str): Type of product. + product_name (str): Name of product. + anatomy (ayon_core.pipeline.Anatomy): Anatomy object. + project_settings (Optional[Dict[str, Any]]): Prepared project settings. + **kwargs: Arbitrary keyword arguments. See below. + + Keyword Arguments: + force_temp (bool): If True, staging dir will be created as tempdir. + always_return_path (bool): If True, staging dir will be created as + tempdir if no staging dir profile is found. + prefix (str): Prefix for staging dir. + suffix (str): Suffix for staging dir. + formatting_data (Dict[str, Any]): Data for formatting staging dir + template. + + Returns: + Dict[str, Any]: Staging dir data + """ + + log = kwargs.get("log") or Logger.get_logger("get_staging_dir") + always_return_path = kwargs.get("always_return_path") + + # make sure always_return_path is set to true by default + if always_return_path is None: + always_return_path = True + + if kwargs.get("force_temp"): + return get_temp_dir( + project_name=project_entity["name"], + anatomy=anatomy, + prefix=kwargs.get("prefix"), + suffix=kwargs.get("suffix"), + ) + + # making fewer queries to database + ctx_data = get_template_data( + project_entity, folder_entity, task_entity, host_name + ) + # add roots to ctx_data + ctx_data["root"] = anatomy.roots + + # add additional data + ctx_data.update({ + "product": { + "type": product_type, + "name": product_name + }, + "host": host_name, + }) + + # add additional data from kwargs + if kwargs.get("formatting_data"): + ctx_data.update(kwargs.get("formatting_data")) + + # get staging dir config + staging_dir_config = get_staging_dir_config( + host_name, + project_entity["name"], + task_entity["type"], + task_entity["name"], + product_type, + product_name, + project_settings=project_settings, + anatomy=anatomy, + log=log, + ) + + # if no preset matching and always_get_some_dir is set, return tempdir + if not staging_dir_config and always_return_path: + return { + "stagingDir": get_temp_dir( + project_name=project_name, + anatomy=anatomy, + prefix=kwargs.get("prefix"), + suffix=kwargs.get("suffix"), + ), + "stagingDir_persistent": False, + } + elif not staging_dir_config: + return None + + return { + "stagingDir": StringTemplate.format_template( + staging_dir_config["template"], ctx_data + ), + "stagingDir_persistent": staging_dir_config["persistence"], + } diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index 29d4659393..a6328135ee 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -3,11 +3,80 @@ Temporary folder operations """ import os +import tempfile +from pathlib import Path from ayon_core.lib import StringTemplate from ayon_core.pipeline import Anatomy -def create_custom_tempdir(project_name, anatomy=None): +def get_temp_dir( + project_name=None, anatomy=None, prefix=None, suffix=None, make_local=False +): + """Get temporary dir path. + + If `make_local` is set, tempdir will be created in local tempdir. + If `anatomy` is not set, default anatomy will be used. + If `prefix` or `suffix` is not set, default values will be used. + + It also supports `OPENPYPE_TMPDIR`, so studio can define own temp + shared repository per project or even per more granular context. + Template formatting is supported also with optional keys. Folder is + created in case it doesn't exists. + + Available anatomy formatting keys: + - root[work | ] + - project[name | code] + + Note: + Staging dir does not have to be necessarily in tempdir so be careful + about its usage. + + Args: + project_name (str)[optional]: Name of project. + anatomy (openpype.pipeline.Anatomy)[optional]: Anatomy object. + make_local (bool)[optional]: If True, temp dir will be created in + local tempdir. + suffix (str)[optional]: Suffix for tempdir. + prefix (str)[optional]: Prefix for tempdir. + + Returns: + str: Path to staging dir of instance. + """ + prefix = prefix or "ay_tmp_" + suffix = suffix or "" + + if make_local: + return _create_local_staging_dir(prefix, suffix) + + # make sure anatomy is set + if not anatomy: + anatomy = Anatomy(project_name) + + # get customized tempdir path from `OPENPYPE_TMPDIR` env var + custom_temp_dir = _create_custom_tempdir(anatomy.project_name, anatomy) + + return _create_local_staging_dir(prefix, suffix, custom_temp_dir) + + +def _create_local_staging_dir(prefix, suffix, dir=None): + """Create local staging dir + + Args: + prefix (str): prefix for tempdir + suffix (str): suffix for tempdir + + Returns: + str: path to tempdir + """ + # use pathlib for creating tempdir + staging_dir = Path(tempfile.mkdtemp( + prefix=prefix, suffix=suffix, dir=dir + )) + + return staging_dir.as_posix() + + +def _create_custom_tempdir(project_name, anatomy=None): """ Create custom tempdir Template path formatting is supporting: @@ -38,7 +107,7 @@ def create_custom_tempdir(project_name, anatomy=None): if anatomy is None: anatomy = Anatomy(project_name) # create base formate data - data = { + template_formatting_data = { "root": anatomy.roots, "project": { "name": anatomy.project_name, @@ -47,19 +116,14 @@ def create_custom_tempdir(project_name, anatomy=None): } # path is anatomy template custom_tempdir = StringTemplate.format_template( - env_tmpdir, data).normalized() + env_tmpdir, template_formatting_data) + + custom_tempdir_path = Path(custom_tempdir) else: # path is absolute - custom_tempdir = env_tmpdir + custom_tempdir_path = Path(env_tmpdir) - # create the dir path if it doesn't exists - if not os.path.exists(custom_tempdir): - try: - # create it if it doesn't exists - os.makedirs(custom_tempdir) - except IOError as error: - raise IOError( - "Path couldn't be created: {}".format(error)) + custom_tempdir_path.mkdir(parents=True, exist_ok=True) - return custom_tempdir + return custom_tempdir_path.as_posix() diff --git a/client/ayon_core/plugins/publish/collect_custom_staging_dir.py b/client/ayon_core/plugins/publish/collect_custom_staging_dir.py deleted file mode 100644 index 49c3a98dd2..0000000000 --- a/client/ayon_core/plugins/publish/collect_custom_staging_dir.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Requires: - anatomy - - -Provides: - instance.data -> stagingDir (folder path) - -> stagingDir_persistent (bool) -""" -import copy -import os.path - -import pyblish.api - -from ayon_core.pipeline.publish.lib import get_custom_staging_dir_info - - -class CollectCustomStagingDir(pyblish.api.InstancePlugin): - """Looks through profiles if stagingDir should be persistent and in special - location. - - Transient staging dir could be useful in specific use cases where is - desirable to have temporary renders in specific, persistent folders, could - be on disks optimized for speed for example. - - It is studio responsibility to clean up obsolete folders with data. - - Location of the folder is configured in `project_anatomy/templates/others`. - ('transient' key is expected, with 'folder' key) - - Which family/task type/product is applicable is configured in: - `project_settings/global/tools/publish/custom_staging_dir_profiles` - - """ - label = "Collect Custom Staging Directory" - order = pyblish.api.CollectorOrder + 0.4990 - - template_key = "transient" - - def process(self, instance): - product_type = instance.data["productType"] - product_name = instance.data["productName"] - host_name = instance.context.data["hostName"] - project_name = instance.context.data["projectName"] - project_settings = instance.context.data["project_settings"] - anatomy = instance.context.data["anatomy"] - task = instance.data["anatomyData"].get("task", {}) - - transient_tml, is_persistent = get_custom_staging_dir_info( - project_name, - host_name, - product_type, - product_name, - task.get("name"), - task.get("type"), - project_settings=project_settings, - anatomy=anatomy, - log=self.log) - - if transient_tml: - anatomy_data = copy.deepcopy(instance.data["anatomyData"]) - anatomy_data["root"] = anatomy.roots - scene_name = instance.context.data.get("currentFile") - if scene_name: - anatomy_data["scene_name"] = os.path.basename(scene_name) - transient_dir = transient_tml.format(**anatomy_data) - instance.data["stagingDir"] = transient_dir - - instance.data["stagingDir_persistent"] = is_persistent - result_str = "Adding '{}' as".format(transient_dir) - else: - result_str = "Not adding" - - self.log.debug("{} custom staging dir for instance with '{}'".format( - result_str, product_type - )) diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 58a032a030..72578d9dc0 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -9,11 +9,13 @@ import clique import pyblish.api from ayon_core import resources, AYON_CORE_ROOT -from ayon_core.pipeline import publish +from ayon_core.pipeline import ( + publish, + get_temp_dir +) from ayon_core.lib import ( run_ayon_launcher_process, - get_transcode_temp_directory, convert_input_paths_for_ffmpeg, should_convert_for_ffmpeg ) @@ -250,7 +252,10 @@ class ExtractBurnin(publish.Extractor): # - change staging dir of source representation # - must be set back after output definitions processing if do_convert: - new_staging_dir = get_transcode_temp_directory() + new_staging_dir = get_temp_dir( + project_name=instance.context.data["projectName"], + make_local=True, + ) repre["stagingDir"] = new_staging_dir convert_input_paths_for_ffmpeg( diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index a28a761e7e..ba173867f8 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -3,15 +3,15 @@ import copy import clique import pyblish.api -from ayon_core.pipeline import publish +from ayon_core.pipeline import ( + publish, + get_temp_dir +) from ayon_core.lib import ( - is_oiio_supported, ) - from ayon_core.lib.transcoding import ( convert_colorspace, - get_transcode_temp_directory, ) from ayon_core.lib.profiles_filtering import filter_profiles @@ -104,7 +104,10 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre = copy.deepcopy(repre) original_staging_dir = new_repre["stagingDir"] - new_staging_dir = get_transcode_temp_directory() + new_staging_dir = get_temp_dir( + project_name=instance.context.data["projectName"], + make_local=True, + ) new_repre["stagingDir"] = new_staging_dir if isinstance(new_repre["files"], list): @@ -254,7 +257,7 @@ class ExtractOIIOTranscode(publish.Extractor): (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] """ pattern = [clique.PATTERNS["frames"]] - collections, remainder = clique.assemble( + collections, _ = clique.assemble( files_to_convert, patterns=pattern, assume_padded_when_ambiguous=True) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 06b451bfbe..26cd2ef0b2 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -22,8 +22,8 @@ from ayon_core.lib.transcoding import ( should_convert_for_ffmpeg, get_review_layer_name, convert_input_paths_for_ffmpeg, - get_transcode_temp_directory, ) +from ayon_core.pipeline import get_temp_dir from ayon_core.pipeline.publish import ( KnownPublishError, get_publish_instance_label, @@ -310,7 +310,10 @@ class ExtractReview(pyblish.api.InstancePlugin): # - change staging dir of source representation # - must be set back after output definitions processing if do_convert: - new_staging_dir = get_transcode_temp_directory() + new_staging_dir = get_temp_dir( + project_name=instance.context.data["projectName"], + make_local=True, + ) repre["stagingDir"] = new_staging_dir convert_input_paths_for_ffmpeg( From 8b1674619ce7004069bed2fa10d8af39ffac3cb6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 12 Sep 2024 16:40:49 +0200 Subject: [PATCH 017/546] Add staging directory functionality and a new plugin for managing staging directories in the pipeline. - Added import statement for 'os' in creator_plugins.py - Implemented method 'apply_staging_dir' to apply staging directory with persistence to instance's transient data in creator_plugins.py - Updated comments and added TODOs related to staging directories in various files - Created a new plugin 'CollectManagedStagingDir' to manage staging directories in publish/lib.py --- .../pipeline/create/creator_plugins.py | 55 +++++++++++++++++++ client/ayon_core/pipeline/publish/lib.py | 2 +- .../publish/collect_managed_staging_dir.py | 43 +++++++++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 client/ayon_core/plugins/publish/collect_managed_staging_dir.py diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 61c10ee736..1360a74519 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import os import copy import collections from typing import TYPE_CHECKING, Optional @@ -14,6 +15,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path ) +from ayon_core.pipeline import get_staging_dir from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -782,6 +784,59 @@ class Creator(BaseCreator): """ return self.pre_create_attr_defs + def apply_staging_dir(self, instance): + """Apply staging dir with persistence to instance's transient data. + + Method is called on instance creation and on instance update. + + Args: + instance (CreatedInstance): Instance for which should be staging + dir applied. + + Returns: + str: Path to staging dir. + """ + create_ctx = self.create_context + product_name = instance.get("productName") + product_type = instance.get("productType") + folder_path = instance.get("folderPath") + if not any([product_name, folder_path]): + return None + + version = instance.get("version") + if version is not None: + formatting_data = {"version": version} + + staging_dir_data = get_staging_dir( + create_ctx.host_name, + create_ctx.get_current_project_entity(), + create_ctx.get_current_folder_entity(), + create_ctx.get_current_task_entity(), + product_type, + product_name, + create_ctx.get_current_project_anatomy(), + create_ctx.get_current_project_settings(), + always_return_path=False, + log=self.log, + formatting_data=formatting_data, + ) + + if not staging_dir_data: + return None + + staging_dir_path = staging_dir_data["stagingDir"] + + # TODO: not sure if this is necessary + # path might be already created by get_staging_dir + if not os.path.exists(staging_dir_path): + os.makedirs(staging_dir_path) + + instance.transient_data.update(staging_dir_data) + + self.log.info(f"Applied staging dir to instance: {staging_dir_path}") + + return staging_dir_path + class HiddenCreator(BaseCreator): @abstractmethod diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 9cfcd3f71a..714794e8f8 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -628,7 +628,7 @@ def get_publish_repre_path(instance, repre, only_published=False): return None -# deprecated: backward compatibility only +# deprecated: backward compatibility only (2024-09-12) # TODO: remove in the future def get_custom_staging_dir_info( project_name, diff --git a/client/ayon_core/plugins/publish/collect_managed_staging_dir.py b/client/ayon_core/plugins/publish/collect_managed_staging_dir.py new file mode 100644 index 0000000000..ca6d5161c1 --- /dev/null +++ b/client/ayon_core/plugins/publish/collect_managed_staging_dir.py @@ -0,0 +1,43 @@ +""" +Requires: + anatomy + + +Provides: + instance.data -> stagingDir (folder path) + -> stagingDir_persistent (bool) +""" + +import pyblish.api + +from ayon_core.pipeline.publish import get_instance_staging_dir + + +class CollectManagedStagingDir(pyblish.api.InstancePlugin): + """Apply matching Staging Dir profile to a instance. + + Apply Staging dir via profiles could be useful in specific use cases + where is desirable to have temporary renders in specific, + persistent folders, could be on disks optimized for speed for example. + + It is studio's responsibility to clean up obsolete folders with data. + + Location of the folder is configured in: + `ayon+anatomy://_/templates/staging`. + + Which family/task type/subset is applicable is configured in: + `ayon+settings://core/tools/publish/custom_staging_dir_profiles` + """ + + label = "Collect Managed Staging Directory" + order = pyblish.api.CollectorOrder + 0.4990 + + def process(self, instance): + + staging_dir_path = get_instance_staging_dir(instance) + persistance = instance.data.get("stagingDir_persistent", False) + + self.log.info(( + f"Instance staging dir was set to `{staging_dir_path}` " + f"and persistence is set to `{persistance}`" + )) From 9e57f74b5c354a3d864eed71f545a5a7f93cc001 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 13 Sep 2024 16:20:46 +0200 Subject: [PATCH 018/546] Update variable names for clarity and consistency. - Renamed variables for better understanding and uniformity - Improved readability by using more descriptive names --- client/ayon_core/pipeline/publish/lib.py | 6 +++--- client/ayon_core/pipeline/stagingdir.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 714794e8f8..fb4db6ddf1 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -696,7 +696,7 @@ def get_instance_staging_dir(instance): workfile_name, _ = os.path.splitext(workfile) formatting_data["workfile_name"] = workfile_name - dir_data = get_staging_dir( + staging_dir_data = get_staging_dir( host_name, project_entity, folder_entity, @@ -708,14 +708,14 @@ def get_instance_staging_dir(instance): formatting_data=formatting_data, ) - staging_dir_path = dir_data["stagingDir"] + staging_dir_path = staging_dir_data["stagingDir"] # TODO: not sure if this is necessary # path might be already created by get_staging_dir if not os.path.exists(staging_dir_path): os.makedirs(staging_dir_path) - instance.data.update(dir_data) + instance.data.update(staging_dir_data) return staging_dir_path diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index e8fa1c4853..5ab9596528 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -202,7 +202,7 @@ def get_staging_dir( if not staging_dir_config and always_return_path: return { "stagingDir": get_temp_dir( - project_name=project_name, + project_name=project_entity["name"], anatomy=anatomy, prefix=kwargs.get("prefix"), suffix=kwargs.get("suffix"), From d01cde7051527c85738b13f1c3769d821e8b0f3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:01:42 +0200 Subject: [PATCH 019/546] reverse disabled and hidden to positive names --- client/ayon_core/lib/attribute_definitions.py | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 894b012d59..a882bee0d9 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -90,6 +90,26 @@ class AbstractAttrDefMeta(ABCMeta): return obj +def _convert_reversed_attr( + main_value, depr_value, main_label, depr_label, default +): + if main_value is not None and depr_value is not None: + if main_value == depr_value: + print( + f"God invalid '{main_label}' and '{depr_label}' arguments." + f" Using '{main_label}' value." + ) + elif depr_value is not None: + print( + f"Using deprecated argument '{depr_label}'" + f" please use '{main_label}' instead." + ) + main_value = not depr_value + elif main_value is None: + main_value = default + return main_value + + class AbstractAttrDef(metaclass=AbstractAttrDefMeta): """Abstraction of attribute definition. @@ -106,12 +126,14 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Args: key (str): Under which key will be attribute value stored. default (Any): Default value of an attribute. - label (str): Attribute label. - tooltip (str): Attribute tooltip. - is_label_horizontal (bool): UI specific argument. Specify if label is - next to value input or ahead. - hidden (bool): Will be item hidden (for UI purposes). - disabled (bool): Item will be visible but disabled (for UI purposes). + label (Optional[str]): Attribute label. + tooltip (Optional[str]): Attribute tooltip. + is_label_horizontal (Optional[bool]): UI specific argument. Specify + if label is next to value input or ahead. + visible (Optional[bool]): Item is shown to user (for UI purposes). + enabled (Optional[bool]): Item is enabled (for UI purposes). + hidden (Optional[bool]): DEPRECATED: Use 'visible' instead. + disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead. """ type_attributes = [] @@ -125,22 +147,28 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): label=None, tooltip=None, is_label_horizontal=None, - hidden=False, - disabled=False + visible=None, + enabled=None, + hidden=None, + disabled=None, ): if is_label_horizontal is None: is_label_horizontal = True - if hidden is None: - hidden = False + enabled = _convert_reversed_attr( + enabled, disabled, "enabled", "disabled", True + ) + visible = _convert_reversed_attr( + visible, hidden, "visible", "hidden", True + ) self.key = key self.label = label self.tooltip = tooltip self.default = default self.is_label_horizontal = is_label_horizontal - self.hidden = hidden - self.disabled = disabled + self.visible = visible + self.enabled = enabled self._id = uuid.uuid4().hex self.__init__class__ = AbstractAttrDef @@ -149,14 +177,30 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self): return self._id + @property + def hidden(self): + return not self.visible + + @hidden.setter + def hidden(self, value): + self.visible = not value + + @property + def disabled(self): + return not self.enabled + + @disabled.setter + def disabled(self, value): + self.enabled = not value + def __eq__(self, other): if not isinstance(other, self.__class__): return False return ( self.key == other.key - and self.hidden == other.hidden and self.default == other.default - and self.disabled == other.disabled + and self.visible == other.visible + and self.enabled == other.enabled ) def __ne__(self, other): @@ -198,8 +242,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): "tooltip": self.tooltip, "default": self.default, "is_label_horizontal": self.is_label_horizontal, - "hidden": self.hidden, - "disabled": self.disabled + "visible": self.visible, + "enabled": self.enabled } for attr in self.type_attributes: data[attr] = getattr(self, attr) @@ -279,8 +323,8 @@ class HiddenDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default - kwargs["hidden"] = True - super(HiddenDef, self).__init__(key, **kwargs) + kwargs["visible"] = False + super().__init__(key, **kwargs) def convert_value(self, value): return value From f174941e1d11fc2d04b85d1de69db8441ff9d6c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:01:54 +0200 Subject: [PATCH 020/546] use new attribute names --- client/ayon_core/tools/attribute_defs/widgets.py | 6 +++--- client/ayon_core/tools/publisher/widgets/widgets.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 5ead3f46a6..026aea00ad 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -28,10 +28,10 @@ from .files_widget import FilesWidget def create_widget_for_attr_def(attr_def, parent=None): widget = _create_widget_for_attr_def(attr_def, parent) - if attr_def.hidden: + if not attr_def.visible: widget.setVisible(False) - if attr_def.disabled: + if not attr_def.enabled: widget.setEnabled(False) return widget @@ -135,7 +135,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): widget = create_widget_for_attr_def(attr_def, self) self._widgets.append(widget) - if attr_def.hidden: + if not attr_def.visible: continue expand_cols = 2 diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 83a2d9e6c1..b0f32dfcfc 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1446,7 +1446,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_instances[attr_def.id] = attr_instances self._attr_def_id_to_attr_def[attr_def.id] = attr_def - if attr_def.hidden: + if not attr_def.visible: continue expand_cols = 2 @@ -1585,7 +1585,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget = create_widget_for_attr_def( attr_def, content_widget ) - hidden_widget = attr_def.hidden + hidden_widget = not attr_def.visible # Hide unknown values of publish plugins # - The keys in most of cases does not represent what would # label represent From 37697bc6ce62f320da88b3bea1f933e424714acd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:06:40 +0200 Subject: [PATCH 021/546] change print to warning --- client/ayon_core/lib/attribute_definitions.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index a882bee0d9..cffa424798 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -4,6 +4,7 @@ import collections import uuid import json import copy +import warnings from abc import ABCMeta, abstractmethod import clique @@ -100,9 +101,13 @@ def _convert_reversed_attr( f" Using '{main_label}' value." ) elif depr_value is not None: - print( - f"Using deprecated argument '{depr_label}'" - f" please use '{main_label}' instead." + warnings.warn( + ( + "DEPRECATION WARNING: Using deprecated argument" + f" '{depr_label}' please use '{main_label}' instead." + ), + DeprecationWarning, + stacklevel=4, ) main_value = not depr_value elif main_value is None: From b6392a3e4201ca3f2dfc75a7a25bb1b3834783be Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:20:15 +0200 Subject: [PATCH 022/546] fix comment Co-authored-by: Roy Nieterau --- client/ayon_core/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index cffa424798..639778b16d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -97,7 +97,7 @@ def _convert_reversed_attr( if main_value is not None and depr_value is not None: if main_value == depr_value: print( - f"God invalid '{main_label}' and '{depr_label}' arguments." + f"Got invalid '{main_label}' and '{depr_label}' arguments." f" Using '{main_label}' value." ) elif depr_value is not None: From 688c25315834f8e7b86090fb19f3da3fe5ad25f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:26:00 +0200 Subject: [PATCH 023/546] modified signature of '_create_instance' --- .../pipeline/create/creator_plugins.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 3b90b11a51..66725e7026 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -364,8 +364,25 @@ class BaseCreator(ABC): return self._log def _create_instance( - self, product_type: str, product_name: str, data: Dict[str, Any] + self, + product_name: str, + data: Dict[str, Any], + product_type: Optional[str] = None ) -> CreatedInstance: + """Create instance and add instance to context. + + Args: + product_name (str): Product name. + data (Dict[str, Any]): Instance data. + product_type (Optional[str]): Product type, object attribute + 'product_type' is used if not passed. + + Returns: + CreatedInstance: Created instance. + + """ + if product_type is None: + product_type = self.product_type instance = CreatedInstance( product_type, product_name, From b905dfe4bbc68e32cd76bb9fdea226c9c68e2a29 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Sep 2024 02:41:30 +0200 Subject: [PATCH 024/546] Support PySide6 --- .../tools/experimental_tools/pyblish_debug_stepper.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index 51b7ba2c06..1db0a3d9f4 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -165,9 +165,9 @@ class DebugUI(QtWidgets.QDialog): text_edit = QtWidgets.QTextEdit() text_edit.setFixedHeight(65) font = QtGui.QFont("NONEXISTENTFONT") - font.setStyleHint(font.TypeWriter) + font.setStyleHint(QtGui.QFont.TypeWriter) text_edit.setFont(font) - text_edit.setLineWrapMode(text_edit.NoWrap) + text_edit.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) step = QtWidgets.QPushButton("Step") step.setEnabled(False) From c9ea3dddd06dcdb15714f977095f49b513ea3f66 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Sep 2024 02:44:36 +0200 Subject: [PATCH 025/546] Cosmetics --- .../pyblish_debug_stepper.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index 1db0a3d9f4..0f1120b8f6 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -82,8 +82,8 @@ class DictChangesModel(QtGui.QStandardItemModel): index = self.index(row, 0, parent_index) if index.data() == key: item = self.itemFromIndex(index) - type_item = self.itemFromIndex(self.index(row, 1, parent_index)) - value_item = self.itemFromIndex(self.index(row, 2, parent_index)) + type_item = self.itemFromIndex(self.index(row, 1, parent_index)) # noqa + value_item = self.itemFromIndex(self.index(row, 2, parent_index)) # noqa break else: item = QtGui.QStandardItem(key) @@ -92,13 +92,16 @@ class DictChangesModel(QtGui.QStandardItemModel): parent.appendRow([item, type_item, value_item]) # Key - key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR + key_color = NEW_KEY_COLOR if key not in previous_data else KEY_COLOR # noqa item.setData(key_color, QtCore.Qt.ForegroundRole) # Type type_str = type(value).__name__ type_color = VALUE_TYPE_COLOR - if key in previous_data and type(previous_data[key]).__name__ != type_str: + if ( + key in previous_data + and type(previous_data[key]).__name__ != type_str + ): type_color = NEW_VALUE_TYPE_COLOR type_item.setText(type_str) @@ -116,23 +119,31 @@ class DictChangesModel(QtGui.QStandardItemModel): if len(value_str) > MAX_VALUE_STR_LEN: value_str = value_str[:MAX_VALUE_STR_LEN] + "..." value_item.setText(value_str) - # Preferably this is deferred to only when the data gets requested - # since this formatting can be slow for very large data sets like - # project settings and system settings - # This will also be MUCH MUCH faster if we don't clear the items on each update - # but only updated/add/remove changed items so that this also runs much less often - value_item.setData(json.dumps(value, default=str, indent=4), QtCore.Qt.ToolTipRole) + # Preferably this is deferred to only when the data gets + # requested since this formatting can be slow for very large + # data sets like project settings and system settings + # This will also be MUCH faster if we don't clear the + # items on each update but only updated/add/remove changed + # items so that this also runs much less often + value_item.setData( + json.dumps(value, default=str, indent=4), + QtCore.Qt.ToolTipRole + ) if isinstance(value, dict): previous_value = previous_data.get(key, {}) if previous_data.get(key) != value: # Update children if the value is not the same as before - self._update_recursive(value, parent=item, previous_data=previous_value) + self._update_recursive(value, + parent=item, + previous_data=previous_value) else: - # TODO: Ensure all children are updated to be not marked as 'changed' - # in the most optimal way possible - self._update_recursive(value, parent=item, previous_data=previous_value) + # TODO: Ensure all children are updated to be not marked + # as 'changed' in the most optimal way possible + self._update_recursive(value, + parent=item, + previous_data=previous_value) self._data = data @@ -195,8 +206,6 @@ class DebugUI(QtWidgets.QDialog): self._previous_data = {} - - def _set_window_title(self, plugin=None): title = "Pyblish Debug Stepper" if plugin is not None: @@ -227,7 +236,7 @@ class DebugUI(QtWidgets.QDialog): self._set_window_title(plugin=result["plugin"]) - print(10*"<" ,result["plugin"].__name__, 10*">") + print(10*"<", result["plugin"].__name__, 10*">") plugin_order = result["plugin"].order plugin_name = result["plugin"].__name__ From b946ed64f33f3c1e19c351b44f8c1d55cf395738 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 24 Sep 2024 02:51:49 +0200 Subject: [PATCH 026/546] Add simple filter field to quickly filter to certain keys only --- .../tools/experimental_tools/pyblish_debug_stepper.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py index 0f1120b8f6..33de4bf036 100644 --- a/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py +++ b/client/ayon_core/tools/experimental_tools/pyblish_debug_stepper.py @@ -185,12 +185,18 @@ class DebugUI(QtWidgets.QDialog): model = DictChangesModel() proxy = QtCore.QSortFilterProxyModel() + proxy.setRecursiveFilteringEnabled(True) proxy.setSourceModel(model) view = QtWidgets.QTreeView() view.setModel(proxy) view.setSortingEnabled(True) + filter_field = QtWidgets.QLineEdit() + filter_field.setPlaceholderText("Filter keys...") + filter_field.textChanged.connect(proxy.setFilterFixedString) + layout.addWidget(text_edit) + layout.addWidget(filter_field) layout.addWidget(view) layout.addWidget(step) @@ -198,6 +204,7 @@ class DebugUI(QtWidgets.QDialog): self._pause = False self.model = model + self.filter = filter_field self.proxy = proxy self.view = view self.text = text_edit From 93033e58a42761bd188a139bd2e27291b185b91a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:39:37 +0200 Subject: [PATCH 027/546] move private method to the bottom --- client/ayon_core/pipeline/create/context.py | 80 ++++++++++----------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 57d24db7db..85ff0557f1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -914,46 +914,6 @@ class CreateContext: _pre_create_data ) - def _create_with_unified_error( - self, identifier, creator, *args, **kwargs - ): - error_message = "Failed to run Creator with identifier \"{}\". {}" - - label = None - add_traceback = False - result = None - fail_info = None - exc_info = None - success = False - - try: - # Try to get creator and his label - if creator is None: - creator = self._get_creator_in_create(identifier) - label = getattr(creator, "label", label) - - # Run create - result = creator.create(*args, **kwargs) - success = True - - except CreatorError: - exc_info = sys.exc_info() - self.log.warning(error_message.format(identifier, exc_info[1])) - - except: # noqa: E722 - add_traceback = True - exc_info = sys.exc_info() - self.log.warning( - error_message.format(identifier, ""), - exc_info=True - ) - - if not success: - fail_info = prepare_failed_creator_operation_info( - identifier, label, exc_info, add_traceback - ) - return result, fail_info - def create_with_unified_error(self, identifier, *args, **kwargs): """Trigger create but raise only one error if anything fails. @@ -1460,3 +1420,43 @@ class CreateContext: if failed_info: raise ConvertorsConversionFailed(failed_info) + + def _create_with_unified_error( + self, identifier, creator, *args, **kwargs + ): + error_message = "Failed to run Creator with identifier \"{}\". {}" + + label = None + add_traceback = False + result = None + fail_info = None + exc_info = None + success = False + + try: + # Try to get creator and his label + if creator is None: + creator = self._get_creator_in_create(identifier) + label = getattr(creator, "label", label) + + # Run create + result = creator.create(*args, **kwargs) + success = True + + except CreatorError: + exc_info = sys.exc_info() + self.log.warning(error_message.format(identifier, exc_info[1])) + + except: # noqa: E722 + add_traceback = True + exc_info = sys.exc_info() + self.log.warning( + error_message.format(identifier, ""), + exc_info=True + ) + + if not success: + fail_info = prepare_failed_creator_operation_info( + identifier, label, exc_info, add_traceback + ) + return result, fail_info From 124cf7b6f689f85990a6098354cc425b091f277b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:42:15 +0200 Subject: [PATCH 028/546] replaced '_remove_instance' with '_remove_instances' --- client/ayon_core/pipeline/create/context.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 85ff0557f1..0a89af1d0d 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -938,9 +938,6 @@ class CreateContext: raise CreatorsCreateFailed([fail_info]) return result - def _remove_instance(self, instance): - self._instances_by_id.pop(instance.id, None) - def creator_removed_instance(self, instance: "CreatedInstance"): """When creator removes instance context should be acknowledged. @@ -952,7 +949,7 @@ class CreateContext: from scene metadata. """ - self._remove_instance(instance) + self._remove_instances([instance]) def add_convertor_item(self, convertor_identifier, label): self.convertor_items_by_id[convertor_identifier] = ConvertorItem( @@ -1310,9 +1307,14 @@ class CreateContext: # Just remove instances from context if creator is not available missing_creators = set(instances_by_identifier) - set(self.creators) + instances = [] for identifier in missing_creators: - for instance in instances_by_identifier[identifier]: - self._remove_instance(instance) + instances.extend( + instance + for instance in instances_by_identifier[identifier] + ) + + self._remove_instances(instances) error_message = "Instances removement of creator \"{}\" failed. {}" failed_info = [] @@ -1421,6 +1423,11 @@ class CreateContext: if failed_info: raise ConvertorsConversionFailed(failed_info) + def _remove_instances(self, instances): + removed_instances = [] + for instance in instances: + obj = self._instances_by_id.pop(instance.id, None) + def _create_with_unified_error( self, identifier, creator, *args, **kwargs ): From 3e37f6ef1592461bf26dc56d22989125aea55a54 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:42:59 +0200 Subject: [PATCH 029/546] added missing imports --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0a89af1d0d..ca27ac32ae 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -7,7 +7,7 @@ import collections import inspect from contextlib import contextmanager import typing -from typing import Optional, Iterable, Dict +from typing import Optional, Iterable, Dict, Any, Callable import pyblish.logic import pyblish.api From 5d982ab5e218f045cf8cdeb63d3c4a34e3b47644 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:44:27 +0200 Subject: [PATCH 030/546] added event hub to context --- client/ayon_core/pipeline/create/context.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index ca27ac32ae..176f3825f1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -15,6 +15,7 @@ import ayon_api from ayon_core.settings import get_project_settings from ayon_core.lib import is_func_signature_supported +from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.attribute_definitions import get_default_values from ayon_core.host import IPublishHost, IWorkfileHost from ayon_core.pipeline import Anatomy @@ -117,6 +118,7 @@ class CreateContext: # Prepare attribute for logger (Created on demand in `log` property) self._log = None + self._event_hub = QueuedEventSystem() # Publish context plugins attributes and it's values self._publish_attributes = PublishAttributes(self, {}) @@ -1423,6 +1425,20 @@ class CreateContext: if failed_info: raise ConvertorsConversionFailed(failed_info) + def _register_event_callback(self, topic: str, callback: Callable): + return self._event_hub.add_callback(topic, callback) + + def _emit_event( + self, + topic: str, + data: Optional[Dict[str, Any]] = None, + sender: Optional[str] = None, + ): + if data is None: + data = {} + data.setdefault("create_context", self) + return self._event_hub.emit(topic, data, sender) + def _remove_instances(self, instances): removed_instances = [] for instance in instances: From febeff61e73a74db4bdece12754d8173e3a7b20d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:26:22 +0200 Subject: [PATCH 031/546] added option to clear callbacks --- client/ayon_core/lib/events.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/lib/events.py b/client/ayon_core/lib/events.py index 2601bc1cf4..1965906dda 100644 --- a/client/ayon_core/lib/events.py +++ b/client/ayon_core/lib/events.py @@ -566,6 +566,10 @@ class EventSystem: self._process_event(event) + def clear_callbacks(self): + """Clear all registered callbacks.""" + self._registered_callbacks = [] + def _process_event(self, event): """Process event topic and trigger callbacks. From a8c842be4ff504df3b11446328ef542c364002d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:26:45 +0200 Subject: [PATCH 032/546] clear callbacks on reset --- client/ayon_core/pipeline/create/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 176f3825f1..3238c70d89 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -503,6 +503,7 @@ class CreateContext: self._collection_shared_data = {} self._folder_id_by_folder_path = {} self._task_names_by_folder_path = {} + self._event_hub.clear_callbacks() def reset_finalization(self): """Cleanup of attributes after reset.""" From 6af9c62d1629ec5111974533f64250658885c5b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:33:18 +0200 Subject: [PATCH 033/546] 'set_publish_plugin_attr_defs' allows to pass in value --- client/ayon_core/pipeline/create/structures.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 18d6d55cfe..a7e5e197d4 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -313,16 +313,24 @@ class PublishAttributes: def origin_data(self): return copy.deepcopy(self._origin_data) - def set_publish_plugin_attr_defs(self, plugin_name, attr_defs): + def set_publish_plugin_attr_defs( + self, + plugin_name: str, + attr_defs: List[AbstractAttrDef], + value: Optional[Dict[str, Any]] = None + ): """Set attribute definitions for plugin. Args: - plugin_name(str): Name of plugin. - attr_defs(Optional[List[AbstractAttrDef]]): Attribute definitions. + plugin_name (str): Name of plugin. + attr_defs (List[AbstractAttrDef]): Attribute definitions. + value (Optional[Dict[str, Any]]): Attribute values. """ # TODO what if 'attr_defs' is 'None'? - value = self._data.get(plugin_name) + if value is None: + value = self._data.get(plugin_name) + if value is None: value = {} From d18079dc2b4fa09b038da008941af08b30d72329 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Sep 2024 17:33:38 +0200 Subject: [PATCH 034/546] trigger event on removed instances --- client/ayon_core/pipeline/create/context.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3238c70d89..95e08bcada 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1444,6 +1444,19 @@ class CreateContext: removed_instances = [] for instance in instances: obj = self._instances_by_id.pop(instance.id, None) + if obj is not None: + removed_instances.append(instance) + + if not removed_instances: + return + + self._emit_event( + "instances.removed", + { + "instances": removed_instances, + "create_context": self, + } + ) def _create_with_unified_error( self, identifier, creator, *args, **kwargs From 921edf213b1ce16ba05e2ac6afb87e4143ea010e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:16:34 +0200 Subject: [PATCH 035/546] moved bulk finishing logic to separate method --- client/ayon_core/pipeline/create/context.py | 27 ++++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 95e08bcada..2cf3410b20 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -977,19 +977,22 @@ class CreateContext: finally: self._bulk_counter -= 1 - # Trigger validation if there is no more context manager for bulk - # instance validation - if self._bulk_counter != 0: - return + self._on_bulk_finished() - ( - self._bulk_instances_to_process, - instances_to_validate - ) = ( - [], - self._bulk_instances_to_process - ) - self.get_instances_context_info(instances_to_validate) + def _on_bulk_finished(self): + # Trigger validation if there is no more context manager for bulk + # instance validation + if self._bulk_counter != 0: + return + + ( + self._bulk_instances_to_process, instances_to_validate + ) = ( + [], self._bulk_instances_to_process + ) + + # Cache folder and task entities for all instances at once + self.get_instances_context_info(instances_to_validate) def reset_instances(self): """Reload instances""" From 352d40f757e319e127b1b67b6da19b4d878a6e36 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:18:31 +0200 Subject: [PATCH 036/546] move publish plugin attributes assignment to bulk finish logic --- client/ayon_core/pipeline/create/context.py | 44 ++++++++++++--------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 2cf3410b20..277eebe7b1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -763,25 +763,6 @@ class CreateContext: self._instances_by_id[instance.id] = instance - publish_attributes = instance.publish_attributes - # Prepare publish plugin attributes and set it on instance - for plugin in self.plugins_with_defs: - if is_func_signature_supported( - plugin.convert_attribute_values, self, instance - ): - plugin.convert_attribute_values(self, instance) - - elif plugin.__instanceEnabled__: - output = plugin.convert_attribute_values(publish_attributes) - if output: - publish_attributes.update(output) - - for plugin in self.plugins_with_defs: - attr_defs = plugin.get_attribute_defs_for_instance(self, instance) - if not attr_defs: - continue - instance.set_publish_plugin_attr_defs(plugin.__name__, attr_defs) - # Add instance to be validated inside 'bulk_instances_collection' # context manager if is inside bulk with self.bulk_instances_collection(): @@ -994,6 +975,31 @@ class CreateContext: # Cache folder and task entities for all instances at once self.get_instances_context_info(instances_to_validate) + for instance in instances_to_validate: + publish_attributes = instance.publish_attributes + # Prepare publish plugin attributes and set it on instance + for plugin in self.plugins_with_defs: + if is_func_signature_supported( + plugin.convert_attribute_values, self, instance + ): + plugin.convert_attribute_values(self, instance) + + elif plugin.__instanceEnabled__: + output = plugin.convert_attribute_values( + publish_attributes) + if output: + publish_attributes.update(output) + + for plugin in self.plugins_with_defs: + attr_defs = plugin.get_attribute_defs_for_instance( + self, instance + ) + if not attr_defs: + continue + instance.set_publish_plugin_attr_defs( + plugin.__name__, attr_defs + ) + def reset_instances(self): """Reload instances""" self._instances_by_id = collections.OrderedDict() From 503b2f579a67ed9bd8407fb6f55e24c7b945282f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:19:00 +0200 Subject: [PATCH 037/546] safe call of methods --- client/ayon_core/pipeline/create/context.py | 40 +++++++++++++++------ 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 277eebe7b1..0082d5c207 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -979,21 +979,39 @@ class CreateContext: publish_attributes = instance.publish_attributes # Prepare publish plugin attributes and set it on instance for plugin in self.plugins_with_defs: - if is_func_signature_supported( + try: + if is_func_signature_supported( plugin.convert_attribute_values, self, instance - ): - plugin.convert_attribute_values(self, instance) + ): + plugin.convert_attribute_values(self, instance) - elif plugin.__instanceEnabled__: - output = plugin.convert_attribute_values( - publish_attributes) - if output: - publish_attributes.update(output) + elif plugin.__instanceEnabled__: + output = plugin.convert_attribute_values( + publish_attributes + ) + if output: + publish_attributes.update(output) + + except Exception: + self.log.error( + "Failed to convert attribute values of" + f" plugin '{plugin.__name__}'", + exc_info=True + ) for plugin in self.plugins_with_defs: - attr_defs = plugin.get_attribute_defs_for_instance( - self, instance - ) + attr_defs = None + try: + attr_defs = plugin.get_attribute_defs_for_instance( + self, instance + ) + except Exception: + self.log.error( + "Failed to get attribute definitions" + f" from plugin '{plugin.__name__}'.", + exc_info=True + ) + if not attr_defs: continue instance.set_publish_plugin_attr_defs( From e379f18620ce83c05493713d76d68f672f108bc4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Sep 2024 10:19:16 +0200 Subject: [PATCH 038/546] emit instance added event --- client/ayon_core/pipeline/create/context.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0082d5c207..56d427a6de 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -975,6 +975,14 @@ class CreateContext: # Cache folder and task entities for all instances at once self.get_instances_context_info(instances_to_validate) + self._emit_event( + "instances.added", + { + "instances": instances_to_validate, + "create_context": self, + } + ) + for instance in instances_to_validate: publish_attributes = instance.publish_attributes # Prepare publish plugin attributes and set it on instance From da8c349aed9792ab603b217fe08e1bb38e67b3eb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:09:40 +0200 Subject: [PATCH 039/546] removed serialization methods from CreatedInstance --- .../ayon_core/pipeline/create/structures.py | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index a7e5e197d4..bbcce54c25 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -791,60 +791,3 @@ class CreatedInstance: if member not in self._members: self._members.append(member) - def serialize_for_remote(self): - """Serialize object into data to be possible recreated object. - - Returns: - Dict[str, Any]: Serialized data. - """ - - creator_attr_defs = self.creator_attributes.get_serialized_attr_defs() - publish_attributes = self.publish_attributes.serialize_attributes() - return { - "data": self.data_to_store(), - "orig_data": self.origin_data, - "creator_attr_defs": creator_attr_defs, - "publish_attributes": publish_attributes, - "creator_label": self._creator_label, - "group_label": self._group_label, - } - - @classmethod - def deserialize_on_remote(cls, serialized_data): - """Convert instance data to CreatedInstance. - - This is fake instance in remote process e.g. in UI process. The creator - is not a full creator and should not be used for calling methods when - instance is created from this method (matters on implementation). - - Args: - serialized_data (Dict[str, Any]): Serialized data for remote - recreating. Should contain 'data' and 'orig_data'. - """ - - instance_data = copy.deepcopy(serialized_data["data"]) - creator_identifier = instance_data["creator_identifier"] - - product_type = instance_data["productType"] - product_name = instance_data.get("productName", None) - - creator_label = serialized_data["creator_label"] - group_label = serialized_data["group_label"] - creator_attr_defs = deserialize_attr_defs( - serialized_data["creator_attr_defs"] - ) - publish_attributes = serialized_data["publish_attributes"] - - obj = cls( - product_type, - product_name, - instance_data, - creator_identifier=creator_identifier, - creator_label=creator_label, - group_label=group_label, - creator_attr_defs=creator_attr_defs - ) - obj._orig_data = serialized_data["orig_data"] - obj.publish_attributes.deserialize_attributes(publish_attributes) - - return obj From 159b1ae9113b4cddaa7b5d65ca30f64f14d5c011 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:35:29 +0200 Subject: [PATCH 040/546] allow only creator passed to created instance --- .../ayon_core/pipeline/create/structures.py | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index bbcce54c25..c65616d3d4 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -409,12 +409,7 @@ class CreatedInstance: product_name (str): Name of product that will be created. data (Dict[str, Any]): Data used for filling product name or override data from already existing instance. - creator (Union[BaseCreator, None]): Creator responsible for instance. - creator_identifier (str): Identifier of creator plugin. - creator_label (str): Creator plugin label. - group_label (str): Default group label from creator plugin. - creator_attr_defs (List[AbstractAttrDef]): Attribute definitions from - creator. + creator (BaseCreator): Creator responsible for instance. """ # Keys that can't be changed or removed from data after loading using @@ -435,16 +430,12 @@ class CreatedInstance: product_type, product_name, data, - creator=None, - creator_identifier=None, - creator_label=None, - group_label=None, - creator_attr_defs=None, + creator, ): - if creator is not None: - creator_identifier = creator.identifier - group_label = creator.get_group_label() - creator_label = creator.label + self._creator = creator + creator_identifier = creator.identifier + group_label = creator.get_group_label() + creator_label = creator.label self._creator_label = creator_label self._group_label = group_label or creator_identifier @@ -504,16 +495,7 @@ class CreatedInstance: # {key: value} creator_values = copy.deepcopy(orig_creator_attributes) - if creator_attr_defs is None: - _creator_attr_defs = [] - else: - _creator_attr_defs = list(creator_attr_defs) - self._data["creator_attributes"] = CreatorAttributeValues( - self, - _creator_attr_defs, - creator_values, - orig_creator_attributes - ) + self._data["creator_attributes"] = creator_values # Stored publish specific attribute values # {: {key: value}} @@ -526,11 +508,10 @@ class CreatedInstance: if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) - if creator is not None: - creator_attr_defs = creator.get_attr_defs_for_instance(self) - self.update_create_attr_defs( - creator_attr_defs, creator_values - ) + creator_attr_defs = creator.get_attr_defs_for_instance(self) + self.update_create_attr_defs( + creator_attr_defs, creator_values + ) def __str__(self): return ( From 40708dd7cefb9343f519dbc2d40471d673a48e59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Sep 2024 17:36:16 +0200 Subject: [PATCH 041/546] added more methods to trigger events --- client/ayon_core/pipeline/create/context.py | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 56d427a6de..da1ea7b9e8 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -960,6 +960,45 @@ class CreateContext: self._on_bulk_finished() + def publish_attribute_value_changed(self, plugin_name, value): + self._emit_event( + "context.values.changed", + { + "publish_attributes": {plugin_name: value} + }, + ) + + # --- instance change callbacks --- + def instance_create_attr_defs_changed(self, instance_id): + # TODO allow bulk changes + self._emit_event( + "instances.create.attr.defs.changed", + { + "instance_ids": [instance_id] + } + ) + + def instance_publish_attr_defs_changed( + self, instance_id, plugin_name + ): + # TODO allow bulk changes + self._emit_event( + "instances.publish.attr.defs.changed", + { + plugin_name: [instance_id], + } + ) + + def instance_values_changed( + self, instance_id, new_values + ): + self._emit_event( + "instances.values.changed", + { + instance_id: new_values + } + ) + def _on_bulk_finished(self): # Trigger validation if there is no more context manager for bulk # instance validation From ca9e016693044c8e0aef35cc9cd281f0c3bdea7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:02:58 +0200 Subject: [PATCH 042/546] structures are triggering events in create context --- .../ayon_core/pipeline/create/structures.py | 126 ++++++++++++------ 1 file changed, 88 insertions(+), 38 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index c65616d3d4..56995ed60b 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -1,9 +1,10 @@ import copy import collections from uuid import uuid4 -from typing import Optional +from typing import Optional, Dict, List, Any from ayon_core.lib.attribute_definitions import ( + AbstractAttrDef, UnknownDef, UIDef, serialize_attr_defs, @@ -80,12 +81,17 @@ class AttributeValues: Has dictionary like methods. Not all of them are allowed all the time. Args: - attr_defs(AbstractAttrDef): Definitions of value type and properties. - values(dict): Values after possible conversion. - origin_data(dict): Values loaded from host before conversion. - """ + parent (Union[CreatedInstance, PublishAttributes]): Parent object. + key (str): Key of attribute values. + attr_defs (List[AbstractAttrDef]): Definitions of value type + and properties. + values (dict): Values after possible conversion. + origin_data (dict): Values loaded from host before conversion. - def __init__(self, attr_defs, values, origin_data=None): + """ + def __init__(self, parent, key, attr_defs, values, origin_data=None): + self._parent = parent + self._key = key if origin_data is None: origin_data = copy.deepcopy(values) self._origin_data = origin_data @@ -148,7 +154,11 @@ class AttributeValues: self._data[_key] = _value changes[_key] = _value + if changes: + self._parent.attribute_value_changed(self._key, changes) + def pop(self, key, default=None): + has_key = key in self._data value = self._data.pop(key, default) # Remove attribute definition if is 'UnknownDef' # - gives option to get rid of unknown values @@ -156,6 +166,8 @@ class AttributeValues: if isinstance(attr_def, UnknownDef): self._attr_defs_by_key.pop(key) self._attr_defs.remove(attr_def) + elif has_key: + self._parent.attribute_value_changed(self._key, {key: None}) return value def reset_values(self): @@ -205,15 +217,11 @@ class AttributeValues: class CreatorAttributeValues(AttributeValues): - """Creator specific attribute values of an instance. + """Creator specific attribute values of an instance.""" - Args: - instance (CreatedInstance): Instance for which are values hold. - """ - - def __init__(self, instance, *args, **kwargs): - self.instance = instance - super().__init__(*args, **kwargs) + @property + def instance(self): + return self._parent class PublishAttributeValues(AttributeValues): @@ -221,19 +229,11 @@ class PublishAttributeValues(AttributeValues): Values are for single plugin which can be on `CreatedInstance` or context values stored on `CreateContext`. - - Args: - publish_attributes(PublishAttributes): Wrapper for multiple publish - attributes is used as parent object. """ - def __init__(self, publish_attributes, *args, **kwargs): - self.publish_attributes = publish_attributes - super().__init__(*args, **kwargs) - @property - def parent(self): - return self.publish_attributes.parent + def publish_attributes(self): + return self._parent class PublishAttributes: @@ -246,12 +246,10 @@ class PublishAttributes: parent(CreatedInstance, CreateContext): Parent for which will be data stored and from which are data loaded. origin_data(dict): Loaded data by plugin class name. - attr_plugins(Union[List[pyblish.api.Plugin], None]): List of publish - plugins that may have defined attribute definitions. - """ + """ def __init__(self, parent, origin_data): - self.parent = parent + self._parent = parent self._origin_data = copy.deepcopy(origin_data) self._data = copy.deepcopy(origin_data) @@ -287,6 +285,7 @@ class PublishAttributes: value = self._data[key] if not isinstance(value, AttributeValues): + self._parent.publish_attribute_value_changed(key, None) return self._data.pop(key) value_item = self._data[key] @@ -294,6 +293,9 @@ class PublishAttributes: output = value_item.data_to_store() # Reset values value_item.reset_values() + self._parent.publish_attribute_value_changed( + key, value_item.data_to_store() + ) return output def mark_as_stored(self): @@ -313,6 +315,9 @@ class PublishAttributes: def origin_data(self): return copy.deepcopy(self._origin_data) + def attribute_value_changed(self, key, changes): + self._parent.publish_attribute_value_changed(key, changes) + def set_publish_plugin_attr_defs( self, plugin_name: str, @@ -342,7 +347,7 @@ class PublishAttributes: value[key] = attr_def.convert_value(value[key]) self._data[plugin_name] = PublishAttributeValues( - self, attr_defs, value, value + self, plugin_name, attr_defs, value, value ) def serialize_attributes(self): @@ -366,7 +371,7 @@ class PublishAttributes: value = data.get(plugin_name) or {} orig_value = copy.deepcopy(origin_data.get(plugin_name) or {}) self._data[plugin_name] = PublishAttributeValues( - self, attr_defs, value, orig_value + self, plugin_name, attr_defs, value, orig_value ) for key, value in data.items(): @@ -532,13 +537,20 @@ class CreatedInstance: def __setitem__(self, key, value): # Validate immutable keys - if key not in self.__immutable_keys: - self._data[key] = value - - elif value != self._data.get(key): + if key in self.__immutable_keys: + if value == self._data.get(key): + return # Raise exception if key is immutable and value has changed raise ImmutableKeyError(key) + if key in self._data and self._data[key] == value: + return + + self._data[key] = value + self._create_context.instance_values_changed( + self.id, {key: value} + ) + def get(self, key, default=None): return self._data.get(key, default) @@ -547,7 +559,13 @@ class CreatedInstance: if key in self.__immutable_keys: raise ImmutableKeyError(key) - self._data.pop(key, *args, **kwargs) + has_key = key in self._data + output = self._data.pop(key, *args, **kwargs) + if has_key: + self._create_context.instance_values_changed( + self.id, {key: None} + ) + return output def keys(self): return self._data.keys() @@ -594,7 +612,7 @@ class CreatedInstance: @property def creator_label(self): - return self._creator_label or self.creator_identifier + return self._creator.label or self.creator_identifier @property def id(self): @@ -711,7 +729,11 @@ class CreatedInstance: continue output[key] = value - output["creator_attributes"] = self.creator_attributes.data_to_store() + if isinstance(self.creator_attributes, AttributeValues): + creator_attributes = self.creator_attributes.data_to_store() + else: + creator_attributes = copy.deepcopy(self.creator_attributes) + output["creator_attributes"] = creator_attributes output["publish_attributes"] = self.publish_attributes.data_to_store() return output @@ -719,13 +741,22 @@ class CreatedInstance: def update_create_attr_defs(self, attr_defs, value=None): if value is not None: value = self._data["creator_attributes"] - origin_data = self._data["creator_attributes"].origin_data + + if isinstance(value, AttributeValues): + value = value.data_to_store() + + if isinstance(self._data["creator_attributes"], AttributeValues): + origin_data = self._data["creator_attributes"].origin_data + else: + origin_data = self._data["creator_attributes"] self._data["creator_attributes"] = CreatorAttributeValues( self, + "creator_attributes", attr_defs, value, origin_data ) + self._create_context.instance_create_attr_defs_changed(self.id) @classmethod def from_existing(cls, instance_data, creator): @@ -753,6 +784,9 @@ class CreatedInstance: product_type, product_name, instance_data, creator ) + def attribute_value_changed(self, key, changes): + self._create_context.instance_values_changed(self.id, {key: changes}) + def set_publish_plugin_attr_defs(self, plugin_name, attr_defs): """Set attribute definitions for publish plugin. @@ -764,6 +798,19 @@ class CreatedInstance: self.publish_attributes.set_publish_plugin_attr_defs( plugin_name, attr_defs ) + self._create_context.instance_publish_attr_defs_changed( + self.id, plugin_name + ) + + def publish_attribute_value_changed(self, plugin_name, value): + self._create_context.instance_values_changed( + self.id, + { + "publish_attributes": { + plugin_name: value, + }, + }, + ) def add_members(self, members): """Currently unused method.""" @@ -772,3 +819,6 @@ class CreatedInstance: if member not in self._members: self._members.append(member) + @property + def _create_context(self): + return self._creator.create_context From 5c0458599d09b862054a4a5069771c05809353fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 24 Sep 2024 17:42:01 +0200 Subject: [PATCH 043/546] don't use CreatedInstance in publisher UI --- client/ayon_core/tools/publisher/abstract.py | 35 +++- client/ayon_core/tools/publisher/control.py | 52 +++-- .../tools/publisher/models/create.py | 196 ++++++++++++++++-- .../publisher/widgets/card_view_widgets.py | 18 +- .../publisher/widgets/list_view_widgets.py | 22 +- .../tools/publisher/widgets/widgets.py | 72 ++++--- client/ayon_core/tools/publisher/window.py | 4 +- 7 files changed, 288 insertions(+), 111 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 6bea4cc247..196c1c938e 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -15,7 +15,6 @@ from ayon_core.lib import AbstractAttrDef from ayon_core.host import HostBase from ayon_core.pipeline.create import ( CreateContext, - CreatedInstance, ConvertorItem, ) from ayon_core.tools.common_models import ( @@ -26,7 +25,7 @@ from ayon_core.tools.common_models import ( ) if TYPE_CHECKING: - from .models import CreatorItem, PublishErrorInfo + from .models import CreatorItem, PublishErrorInfo, InstanceItem class CardMessageTypes: @@ -307,19 +306,19 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): pass @abstractmethod - def get_instances(self) -> List[CreatedInstance]: + def get_instance_items(self) -> List["InstanceItem"]: """Collected/created instances. Returns: - List[CreatedInstance]: List of created instances. + List[InstanceItem]: List of created instances. """ pass @abstractmethod - def get_instances_by_id( + def get_instance_items_by_id( self, instance_ids: Optional[Iterable[str]] = None - ) -> Dict[str, Union[CreatedInstance, None]]: + ) -> Dict[str, Union["InstanceItem", None]]: pass @abstractmethod @@ -334,22 +333,38 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod def get_creator_attribute_definitions( - self, instances: List[CreatedInstance] - ) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]: + self, instance_ids: List[str] + ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: + pass + + @abstractmethod + def set_instances_create_attr_values( + self, instance_ids: Iterable[str], key: str, value: Any + ): pass @abstractmethod def get_publish_attribute_definitions( self, - instances: List[CreatedInstance], + instance_ids: List[str], include_context: bool ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[CreatedInstance, Any]]] + Dict[str, List[Tuple[str, Any]]] ]]: pass + @abstractmethod + def set_instances_publish_attr_values( + self, + instance_ids: Iterable[str], + plugin_name: str, + key: str, + value: Any + ): + pass + @abstractmethod def get_product_name( self, diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index c7fd75b3c3..8664cfe605 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -172,23 +172,25 @@ class PublisherController( """ return self._create_model.get_creator_icon(identifier) + def get_instance_items(self): + """Current instances in create context.""" + return self._create_model.get_instance_items() + + # --- Legacy for TrayPublisher --- @property def instances(self): - """Current instances in create context. - - Deprecated: - Use 'get_instances' instead. Kept for backwards compatibility with - traypublisher. - - """ - return self.get_instances() + return self.get_instance_items() def get_instances(self): - """Current instances in create context.""" - return self._create_model.get_instances() + return self.get_instance_items() - def get_instances_by_id(self, instance_ids=None): - return self._create_model.get_instances_by_id(instance_ids) + def get_instances_by_id(self, *args, **kwargs): + return self.get_instance_items_by_id(*args, **kwargs) + + # --- + + def get_instance_items_by_id(self, instance_ids=None): + return self._create_model.get_instance_items_by_id(instance_ids) def get_instances_context_info(self, instance_ids=None): return self._create_model.get_instances_context_info(instance_ids) @@ -365,29 +367,41 @@ class PublisherController( if os.path.exists(dirpath): shutil.rmtree(dirpath) - def get_creator_attribute_definitions(self, instances): + def get_creator_attribute_definitions(self, instance_ids): """Collect creator attribute definitions for multuple instances. Args: - instances(List[CreatedInstance]): List of created instances for + instance_ids (List[str]): List of created instances for which should be attribute definitions returned. """ return self._create_model.get_creator_attribute_definitions( - instances + instance_ids ) - def get_publish_attribute_definitions(self, instances, include_context): + def set_instances_create_attr_values(self, instance_ids, key, value): + return self._create_model.set_instances_create_attr_values( + instance_ids, key, value + ) + + def get_publish_attribute_definitions(self, instance_ids, include_context): """Collect publish attribute definitions for passed instances. Args: - instances(list): List of created instances for + instance_ids (List[str]): List of created instances for which should be attribute definitions returned. - include_context(bool): Add context specific attribute definitions. + include_context (bool): Add context specific attribute definitions. """ return self._create_model.get_publish_attribute_definitions( - instances, include_context + instance_ids, include_context + ) + + def set_instances_publish_attr_values( + self, instance_ids, plugin_name, key, value + ): + return self._create_model.set_instances_publish_attr_values( + instance_ids, plugin_name, key, value ) def get_product_name( diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index dcd2ce4acc..9d9d13fda6 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -192,6 +192,110 @@ class CreatorItem: return cls(**data) +class InstanceItem: + def __init__( + self, + instance_id: str, + creator_identifier: str, + label: str, + group_label: str, + product_type: str, + product_name: str, + variant: str, + folder_path: Optional[str], + task_name: Optional[str], + is_active: bool, + has_promised_context: bool, + ): + self._instance_id: str = instance_id + self._creator_identifier: str = creator_identifier + self._label: str = label + self._group_label: str = group_label + self._product_type: str = product_type + self._product_name: str = product_name + self._variant: str = variant + self._folder_path: Optional[str] = folder_path + self._task_name: Optional[str] = task_name + self._is_active: bool = is_active + self._has_promised_context: bool = has_promised_context + + @property + def id(self): + return self._instance_id + + @property + def creator_identifier(self): + return self._creator_identifier + + @property + def label(self): + return self._label + + @property + def group_label(self): + return self._group_label + + @property + def product_type(self): + return self._product_type + + @property + def has_promised_context(self): + return self._has_promised_context + + def get_variant(self): + return self._variant + + def set_variant(self, variant): + self._variant = variant + + def get_product_name(self): + return self._product_name + + def set_product_name(self, product_name): + self._product_name = product_name + + def get_folder_path(self): + return self._folder_path + + def set_folder_path(self, folder_path): + self._folder_path = folder_path + + def get_task_name(self): + return self._task_name + + def set_task_name(self, task_name): + self._task_name = task_name + + def get_is_active(self): + return self._is_active + + def set_is_active(self, is_active): + self._is_active = is_active + + product_name = property(get_product_name, set_product_name) + variant = property(get_variant, set_variant) + folder_path = property(get_folder_path, set_folder_path) + task_name = property(get_task_name, set_task_name) + is_active = property(get_is_active, set_is_active) + + @classmethod + def from_instance(cls, instance: CreatedInstance): + return InstanceItem( + instance.id, + instance.creator_identifier, + instance.label, + instance.group_label, + instance.product_type, + instance.product_name, + instance["variant"], + instance["folderPath"], + instance["task"], + instance["active"], + instance.has_promised_context, + ) + + class CreateModel: def __init__(self, controller: AbstractPublisherBackend): self._log = None @@ -287,29 +391,36 @@ class CreateModel: return creator_item.icon return None - def get_instances(self) -> List[CreatedInstance]: + def get_instance_items(self) -> List[InstanceItem]: """Current instances in create context.""" - return list(self._create_context.instances_by_id.values()) + return [ + InstanceItem.from_instance(instance) + for instance in self._create_context.instances_by_id.values() + ] - def get_instance_by_id( + def get_instance_item_by_id( self, instance_id: str - ) -> Union[CreatedInstance, None]: - return self._create_context.instances_by_id.get(instance_id) + ) -> Union[InstanceItem, None]: + instance = self._create_context.instances_by_id.get(instance_id) + if instance is None: + return None - def get_instances_by_id( + return InstanceItem.from_instance(instance) + + def get_instance_items_by_id( self, instance_ids: Optional[Iterable[str]] = None - ) -> Dict[str, Union[CreatedInstance, None]]: + ) -> Dict[str, Union[InstanceItem, None]]: if instance_ids is None: instance_ids = self._create_context.instances_by_id.keys() return { - instance_id: self.get_instance_by_id(instance_id) + instance_id: self.get_instance_item_by_id(instance_id) for instance_id in instance_ids } def get_instances_context_info( self, instance_ids: Optional[Iterable[str]] = None ): - instances = self.get_instances_by_id(instance_ids).values() + instances = self._get_instances_by_id(instance_ids).values() return self._create_context.get_instances_context_info( instances ) @@ -341,7 +452,7 @@ class CreateModel: instance = None if instance_id: - instance = self.get_instance_by_id(instance_id) + instance = self._get_instance_by_id(instance_id) project_name = self._controller.get_current_project_name() folder_item = self._controller.get_folder_item_by_path( @@ -500,21 +611,30 @@ class CreateModel: self._on_create_instance_change() + def set_instances_create_attr_values(self, instance_ids, key, value): + # TODO set bulk change context + for instance_id in instance_ids: + instance = self._get_instance_by_id(instance_id) + creator_attributes = instance["creator_attributes"] + if key in creator_attributes: + creator_attributes[key] = value + def get_creator_attribute_definitions( - self, instances: List[CreatedInstance] - ) -> List[Tuple[AbstractAttrDef, List[CreatedInstance], List[Any]]]: + self, instance_ids: List[str] + ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: """Collect creator attribute definitions for multuple instances. Args: - instances (List[CreatedInstance]): List of created instances for + instance_ids (List[str]): List of created instances for which should be attribute definitions returned. - """ + """ # NOTE it would be great if attrdefs would have hash method implemented # so they could be used as keys in dictionary output = [] _attr_defs = {} - for instance in instances: + for instance_id in instance_ids: + instance = self._get_instance_by_id(instance_id) for attr_def in instance.creator_attribute_defs: found_idx = None for idx, _attr_def in _attr_defs.items(): @@ -527,27 +647,39 @@ class CreateModel: value = instance.creator_attributes[attr_def.key] if found_idx is None: idx = len(output) - output.append((attr_def, [instance], [value])) + output.append((attr_def, [instance_id], [value])) _attr_defs[idx] = attr_def else: item = output[found_idx] - item[1].append(instance) + item[1].append(instance_id) item[2].append(value) return output + def set_instances_publish_attr_values( + self, instance_ids, plugin_name, key, value + ): + # TODO set bulk change context + for instance_id in instance_ids: + if instance_id is None: + instance = self._create_context + else: + instance = self._get_instance_by_id(instance_id) + plugin_val = instance.publish_attributes[plugin_name] + plugin_val[key] = value + def get_publish_attribute_definitions( self, - instances: List[CreatedInstance], + instance_ids: List[str], include_context: bool ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[CreatedInstance, Any]]] + Dict[str, List[Tuple[str, Any]]] ]]: """Collect publish attribute definitions for passed instances. Args: - instances (list[CreatedInstance]): List of created instances for + instance_ids (List[str]): List of created instances for which should be attribute definitions returned. include_context (bool): Add context specific attribute definitions. @@ -556,12 +688,15 @@ class CreateModel: if include_context: _tmp_items.append(self._create_context) - for instance in instances: - _tmp_items.append(instance) + for instance_id in instance_ids: + _tmp_items.append(self._get_instance_by_id(instance_id)) all_defs_by_plugin_name = {} all_plugin_values = {} for item in _tmp_items: + item_id = None + if isinstance(item, CreatedInstance): + item_id = item.id for plugin_name, attr_val in item.publish_attributes.items(): attr_defs = attr_val.attr_defs if not attr_defs: @@ -579,7 +714,7 @@ class CreateModel: attr_values = plugin_values.setdefault(attr_def.key, []) value = attr_val[attr_def.key] - attr_values.append((item, value)) + attr_values.append((item_id, value)) output = [] for plugin in self._create_context.plugins_with_defs: @@ -638,6 +773,21 @@ class CreateModel: return self._create_context.creators + def _get_instance_by_id( + self, instance_id: str + ) -> Union[CreatedInstance, None]: + return self._create_context.instances_by_id.get(instance_id) + + def _get_instances_by_id( + self, instance_ids: Optional[Iterable[str]] + ) -> Dict[str, Union[CreatedInstance, None]]: + if instance_ids is None: + instance_ids = self._create_context.instances_by_id.keys() + return { + instance_id: self._get_instance_by_id(instance_id) + for instance_id in instance_ids + } + def _reset_instances(self): """Reset create instances.""" diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index c0e27d9c60..6ef34b86f8 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -227,7 +227,7 @@ class InstanceGroupWidget(BaseGroupWidget): """Update instances for the group. Args: - instances (list[CreatedInstance]): List of instances in + instances (list[InstanceItem]): List of instances in CreateContext. context_info_by_id (Dict[str, InstanceContextInfo]): Instance context info by instance id. @@ -238,7 +238,7 @@ class InstanceGroupWidget(BaseGroupWidget): instances_by_product_name = collections.defaultdict(list) for instance in instances: instances_by_id[instance.id] = instance - product_name = instance["productName"] + product_name = instance.product_name instances_by_product_name[product_name].append(instance) # Remove instance widgets that are not in passed instances @@ -473,12 +473,12 @@ class InstanceCardWidget(CardWidget): def set_active(self, new_value): """Set instance as active.""" checkbox_value = self._active_checkbox.isChecked() - instance_value = self.instance["active"] + instance_value = self.instance.is_active # First change instance value and them change checkbox # - prevent to trigger `active_changed` signal if instance_value != new_value: - self.instance["active"] = new_value + self.instance.is_active = new_value if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) @@ -494,8 +494,8 @@ class InstanceCardWidget(CardWidget): self._context_warning.setVisible(not valid) def _update_product_name(self): - variant = self.instance["variant"] - product_name = self.instance["productName"] + variant = self.instance.variant + product_name = self.instance.product_name label = self.instance.label if ( variant == self._last_variant @@ -525,7 +525,7 @@ class InstanceCardWidget(CardWidget): def update_instance_values(self, context_info): """Update instance data""" self._update_product_name() - self.set_active(self.instance["active"]) + self.set_active(self.instance.is_active) self._validate_context(context_info) def _set_expanded(self, expanded=None): @@ -535,11 +535,11 @@ class InstanceCardWidget(CardWidget): def _on_active_change(self): new_value = self._active_checkbox.isChecked() - old_value = self.instance["active"] + old_value = self.instance.is_active if new_value == old_value: return - self.instance["active"] = new_value + self.instance.is_active = new_value self.active_changed.emit(self._id, new_value) def _on_expend_clicked(self): diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index ab9f2db52c..14814a4aa6 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -131,7 +131,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): product_name_label.setObjectName("ListViewProductName") active_checkbox = NiceCheckbox(parent=self) - active_checkbox.setChecked(instance["active"]) + active_checkbox.setChecked(instance.is_active) layout = QtWidgets.QHBoxLayout(self) content_margins = layout.contentsMargins() @@ -171,19 +171,19 @@ class InstanceListItemWidget(QtWidgets.QWidget): def is_active(self): """Instance is activated.""" - return self.instance["active"] + return self.instance.is_active def set_active(self, new_value): """Change active state of instance and checkbox.""" checkbox_value = self._active_checkbox.isChecked() - instance_value = self.instance["active"] + instance_value = self.instance.is_active if new_value is None: new_value = not instance_value # First change instance value and them change checkbox # - prevent to trigger `active_changed` signal if instance_value != new_value: - self.instance["active"] = new_value + self.instance.is_active = new_value if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) @@ -200,17 +200,17 @@ class InstanceListItemWidget(QtWidgets.QWidget): if label != self._instance_label_widget.text(): self._instance_label_widget.setText(html_escape(label)) # Check active state - self.set_active(self.instance["active"]) + self.set_active(self.instance.is_active) # Check valid states self._set_valid_property(context_info.is_valid) def _on_active_change(self): new_value = self._active_checkbox.isChecked() - old_value = self.instance["active"] + old_value = self.instance.is_active if new_value == old_value: return - self.instance["active"] = new_value + self.instance.is_active = new_value self.active_changed.emit(self.instance.id, new_value) def set_active_toggle_enabled(self, enabled): @@ -639,10 +639,10 @@ class InstanceListView(AbstractInstanceView): instance_id = instance.id # Handle group activity if activity is None: - activity = int(instance["active"]) + activity = int(instance.is_active) elif activity == -1: pass - elif activity != instance["active"]: + elif activity != instance.is_active: activity = -1 context_info = context_info_by_id[instance_id] @@ -658,8 +658,8 @@ class InstanceListView(AbstractInstanceView): # Create new item and store it as new item = QtGui.QStandardItem() - item.setData(instance["productName"], SORT_VALUE_ROLE) - item.setData(instance["productName"], GROUP_ROLE) + item.setData(instance.product_name, SORT_VALUE_ROLE) + item.setData(instance.product_name, GROUP_ROLE) item.setData(instance_id, INSTANCE_ID_ROLE) new_items.append(item) new_items_with_instance.append((item, instance)) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 83a2d9e6c1..429f7e0472 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1186,9 +1186,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if instance.has_promised_context: continue - new_variant_value = instance.get("variant") - new_folder_path = instance.get("folderPath") - new_task_name = instance.get("task") + new_variant_value = instance.variant + new_folder_path = instance.folder_path + new_task_name = instance.task_name if variant_value is not None: new_variant_value = variant_value @@ -1210,20 +1210,20 @@ class GlobalAttrsWidget(QtWidgets.QWidget): except TaskNotSetError: invalid_tasks = True - product_names.add(instance["productName"]) + product_names.add(instance.product_name) continue product_names.add(new_product_name) if variant_value is not None: - instance["variant"] = variant_value + instance.variant = variant_value if folder_path is not None: - instance["folderPath"] = folder_path + instance.folder_path = folder_path if task_name is not None: - instance["task"] = task_name or None + instance.task_name = task_name or None - instance["productName"] = new_product_name + instance.product_name = new_product_name if invalid_tasks: self.task_value_widget.set_invalid_empty_task() @@ -1290,7 +1290,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): """Set currently selected instances. Args: - instances(List[CreatedInstance]): List of selected instances. + instances (List[InstanceItem]): List of selected instances. Empty instances tells that nothing or context is selected. """ self._set_btns_visible(False) @@ -1318,13 +1318,13 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if instance.creator_identifier is None: editable = False - variants.add(instance.get("variant") or self.unknown_value) - product_types.add(instance.get("productType") or self.unknown_value) - folder_path = instance.get("folderPath") or self.unknown_value - task_name = instance.get("task") or "" + variants.add(instance.variant or self.unknown_value) + product_types.add(instance.product_type or self.unknown_value) + folder_path = instance.folder_path or self.unknown_value + task_name = instance.task_name or "" folder_paths.add(folder_path) folder_task_combinations.append((folder_path, task_name)) - product_names.add(instance.get("productName") or self.unknown_value) + product_names.add(instance.product_name or self.unknown_value) if not editable: context_editable = False @@ -1406,7 +1406,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): ): self._content_widget.setEnabled(valid) - def set_current_instances(self, instances): + def set_current_instances(self, instance_ids): """Set current instances for which are attribute definitions shown.""" prev_content_widget = self._scroll_area.widget() @@ -1420,7 +1420,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_attr_def = {} result = self._controller.get_creator_attribute_definitions( - instances + instance_ids ) content_widget = QtWidgets.QWidget(self._scroll_area) @@ -1432,7 +1432,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) row = 0 - for attr_def, attr_instances, values in result: + for attr_def, instance_ids, values in result: widget = create_widget_for_attr_def(attr_def, content_widget) if attr_def.is_value_def: if len(values) == 1: @@ -1443,7 +1443,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widget.set_value(values, True) widget.value_changed.connect(self._input_value_changed) - self._attr_def_id_to_instances[attr_def.id] = attr_instances + self._attr_def_id_to_instances[attr_def.id] = instance_ids self._attr_def_id_to_attr_def[attr_def.id] = attr_def if attr_def.hidden: @@ -1483,15 +1483,13 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._content_widget = content_widget def _input_value_changed(self, value, attr_id): - instances = self._attr_def_id_to_instances.get(attr_id) + instance_ids = self._attr_def_id_to_instances.get(attr_id) attr_def = self._attr_def_id_to_attr_def.get(attr_id) - if not instances or not attr_def: + if not instance_ids or not attr_def: return - - for instance in instances: - creator_attributes = instance["creator_attributes"] - if attr_def.key in creator_attributes: - creator_attributes[attr_def.key] = value + self._controller.set_instances_create_attr_values( + instance_ids, attr_def.key, value + ) class PublishPluginAttrsWidget(QtWidgets.QWidget): @@ -1546,7 +1544,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): ): self._content_widget.setEnabled(valid) - def set_current_instances(self, instances, context_selected): + def set_current_instances(self, instance_ids, context_selected): """Set current instances for which are attribute definitions shown.""" prev_content_widget = self._scroll_area.widget() @@ -1562,7 +1560,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_plugin_name = {} result = self._controller.get_publish_attribute_definitions( - instances, context_selected + instance_ids, context_selected ) content_widget = QtWidgets.QWidget(self._scroll_area) @@ -1648,15 +1646,15 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._content_widget = content_widget def _input_value_changed(self, value, attr_id): - instances = self._attr_def_id_to_instances.get(attr_id) + instance_ids = self._attr_def_id_to_instances.get(attr_id) attr_def = self._attr_def_id_to_attr_def.get(attr_id) plugin_name = self._attr_def_id_to_plugin_name.get(attr_id) - if not instances or not attr_def or not plugin_name: + if not instance_ids or not attr_def or not plugin_name: return - for instance in instances: - plugin_val = instance.publish_attributes[plugin_name] - plugin_val[attr_def.key] = value + self._controller.set_instances_publish_attr_values( + instance_ids, plugin_name, attr_def.key, value + ) class ProductAttributesWidget(QtWidgets.QWidget): @@ -1821,10 +1819,10 @@ class ProductAttributesWidget(QtWidgets.QWidget): """Change currently selected items. Args: - instances(List[CreatedInstance]): List of currently selected + instances (List[InstanceItem]): List of currently selected instances. - context_selected(bool): Is context selected. - convertor_identifiers(List[str]): Identifiers of convert items. + context_selected (bool): Is context selected. + convertor_identifiers (List[str]): Identifiers of convert items. """ instance_ids = { @@ -1849,9 +1847,9 @@ class ProductAttributesWidget(QtWidgets.QWidget): self._convert_widget.setVisible(len(s_convertor_identifiers) > 0) self.global_attrs_widget.set_current_instances(instances) - self.creator_attrs_widget.set_current_instances(instances) + self.creator_attrs_widget.set_current_instances(instance_ids) self.publish_attrs_widget.set_current_instances( - instances, context_selected + instance_ids, context_selected ) self.creator_attrs_widget.set_instances_valid(all_valid) self.publish_attrs_widget.set_instances_valid(all_valid) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 434c2ca602..e4da71b3d6 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -918,8 +918,8 @@ class PublisherWindow(QtWidgets.QDialog): active_instances_by_id = { instance.id: instance - for instance in self._controller.get_instances() - if instance["active"] + for instance in self._controller.get_instance_items() + if instance.is_active } context_info_by_id = self._controller.get_instances_context_info( active_instances_by_id.keys() From 50305d7004991858d74956a65e7136f4a756b5e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Sep 2024 17:19:53 +0200 Subject: [PATCH 044/546] use bulk event emit for any changes --- client/ayon_core/pipeline/create/context.py | 256 ++++++++++++++---- .../ayon_core/pipeline/create/structures.py | 4 +- .../tools/publisher/models/create.py | 32 ++- 3 files changed, 222 insertions(+), 70 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index da1ea7b9e8..921ff9d2ad 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -7,7 +7,16 @@ import collections import inspect from contextlib import contextmanager import typing -from typing import Optional, Iterable, Dict, Any, Callable +from typing import ( + Optional, + Iterable, + Tuple, + List, + Dict, + Any, + Callable, + Union, +) import pyblish.logic import pyblish.api @@ -92,6 +101,38 @@ def prepare_failed_creator_operation_info( } +class BulkInfo: + def __init__(self): + self._count = 0 + self._data = [] + self._sender = None + + def __bool__(self): + return self._count == 0 + + def get_sender(self): + return self._sender + + def set_sender(self, sender): + if sender is not None: + self._sender = sender + + def increase(self): + self._count += 1 + + def decrease(self): + self._count -= 1 + + def append(self, item): + self._data.append(item) + + def pop_data(self): + data = self._data + self._data = [] + self._sender = None + return data + + class CreateContext: """Context of instance creation. @@ -181,8 +222,17 @@ class CreateContext: # - they can be validation for multiple instances at one time # using context manager which will trigger validation # after leaving of last context manager scope - self._bulk_counter = 0 - self._bulk_instances_to_process = [] + self._bulk_info = { + # Collect instances + "collect": BulkInfo(), + # Change values of instances or create context + "change": BulkInfo(), + # Create attribute definitions changed + "create_attrs_change": BulkInfo(), + # Publish attribute definitions changed + "publish_attrs_change": BulkInfo(), + } + self._bulk_order = [] # Shared data across creators during collection phase self._collection_shared_data = None @@ -765,8 +815,8 @@ class CreateContext: # Add instance to be validated inside 'bulk_instances_collection' # context manager if is inside bulk - with self.bulk_instances_collection(): - self._bulk_instances_to_process.append(instance) + with self.bulk_instances_collection() as bulk_info: + bulk_info.append(instance) def _get_creator_in_create(self, identifier): """Creator by identifier with unified error. @@ -944,72 +994,98 @@ class CreateContext: self.convertor_items_by_id.pop(convertor_identifier, None) @contextmanager - def bulk_instances_collection(self): - """Validate context of instances in bulk. + def bulk_instances_collection(self, sender=None): + with self._bulk_context("collect", sender) as bulk_info: + yield bulk_info - This can be used for single instance or for adding multiple instances - which is helpfull on reset. + @contextmanager + def bulk_value_changes(self, sender=None): + with self._bulk_context("change", sender) as bulk_info: + yield bulk_info - Should not be executed from multiple threads. - """ - self._bulk_counter += 1 - try: - yield - finally: - self._bulk_counter -= 1 + @contextmanager + def bulk_create_attr_defs_change(self, sender=None): + with self._bulk_context("create_attrs_change", sender) as bulk_info: + yield bulk_info - self._on_bulk_finished() - - def publish_attribute_value_changed(self, plugin_name, value): - self._emit_event( - "context.values.changed", - { - "publish_attributes": {plugin_name: value} - }, - ) + @contextmanager + def bulk_publish_attr_defs_change(self, sender=None): + with self._bulk_context("publish_attrs_change", sender) as bulk_info: + yield bulk_info # --- instance change callbacks --- def instance_create_attr_defs_changed(self, instance_id): - # TODO allow bulk changes - self._emit_event( - "instances.create.attr.defs.changed", - { - "instance_ids": [instance_id] - } - ) + with self.bulk_create_attr_defs_change() as bulk_item: + bulk_item.append(instance_id) def instance_publish_attr_defs_changed( self, instance_id, plugin_name ): - # TODO allow bulk changes - self._emit_event( - "instances.publish.attr.defs.changed", - { - plugin_name: [instance_id], - } - ) + with self.bulk_publish_attr_defs_change() as bulk_item: + bulk_item.append((instance_id, plugin_name)) def instance_values_changed( self, instance_id, new_values ): - self._emit_event( - "instances.values.changed", + with self.bulk_value_changes() as bulk_item: + bulk_item.append((instance_id, new_values)) + + # --- context change callbacks --- + def publish_attribute_value_changed(self, plugin_name, value): + self.instance_values_changed( + None, { - instance_id: new_values - } + "publish_attributes": { + plugin_name: value, + }, + }, ) - def _on_bulk_finished(self): - # Trigger validation if there is no more context manager for bulk - # instance validation - if self._bulk_counter != 0: + @contextmanager + def _bulk_context(self, key, sender): + bulk_info = self._bulk_info[key] + bulk_info.set_sender(sender) + + bulk_info.increase() + if key not in self._bulk_order: + self._bulk_order.append(key +) + try: + yield bulk_info + finally: + bulk_info.decrease() + if bulk_info: + self._bulk_finished(key) + + def _bulk_finished(self, key): + if self._bulk_order[0] != key: return - ( - self._bulk_instances_to_process, instances_to_validate - ) = ( - [], self._bulk_instances_to_process - ) + self._bulk_finish(key) + self._bulk_order.pop(0) + + for key in tuple(self._bulk_order): + if not self._bulk_info[key]: + return + self._bulk_finish(key) + self._bulk_order.pop(0) + + def _bulk_finish(self, key): + bulk_info = self._bulk_info[key] + sender = bulk_info.get_sender() + data = bulk_info.pop_data() + if key == "collect": + self._bulk_instances_collection(data, sender) + elif key == "change": + self._bulk_values_change(data, sender) + elif key == "create_attrs_change": + self._bulk_create_attrs_change(data, sender) + elif key == "publish_attrs_change": + self._bulk_publish_attrs_change(data, sender) + + def _bulk_instances_collection(self, instances_to_validate, sender): + if not instances_to_validate: + return # Cache folder and task entities for all instances at once self.get_instances_context_info(instances_to_validate) @@ -1019,7 +1095,8 @@ class CreateContext: { "instances": instances_to_validate, "create_context": self, - } + }, + sender, ) for instance in instances_to_validate: @@ -1065,6 +1142,76 @@ class CreateContext: plugin.__name__, attr_defs ) + def _bulk_values_change( + self, + changes: Tuple[Union[str, None], Dict[str, Any]], + sender: Optional[str], + ): + if not changes: + return + event_data = {} + for item_id, item_changes in changes: + item_values = event_data.setdefault(item_id, {}) + if "creator_attributes" in item_changes: + current_value = item_values.setdefault( + "creator_attributes", {} + ) + current_value.update( + item_changes.pop("creator_attributes") + ) + + if "publish_attributes" in item_changes: + current_publish = item_values.setdefault( + "publish_attributes", {} + ) + for plugin_name, plugin_value in item_changes.pop( + "publish_attributes" + ).items(): + plugin_changes = current_publish.setdefault( + plugin_name, {} + ) + plugin_changes.update(plugin_value) + + item_values.update(item_changes) + + self._emit_event( + "values.changed", + event_data, + sender + ) + + def _bulk_create_attrs_change( + self, data: List[str], sender: Optional[str] + ): + if not data: + return + + self._emit_event( + "create.attr.defs.changed", + { + "instance_ids": set(data) + }, + sender, + ) + + def _bulk_publish_attrs_change( + self, + attr_info: Tuple[str, Union[str, None]], + sender: Optional[str], + ): + if not attr_info: + return + event_data = {} + for plugin_name, instance_id in attr_info: + instance_ids = event_data.setdefault(plugin_name, set()) + instance_ids.add(instance_id) + + self._emit_event( + "publish.attr.defs.changed", + event_data, + sender, + ) + def reset_instances(self): """Reload instances""" self._instances_by_id = collections.OrderedDict() @@ -1416,6 +1563,9 @@ class CreateContext: error_message.format(identifier, exc_info[1]) ) + except (KeyboardInterrupt, SystemExit): + raise + except: # noqa: E722 failed = True add_traceback = True diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 56995ed60b..79a1e61a55 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -285,7 +285,7 @@ class PublishAttributes: value = self._data[key] if not isinstance(value, AttributeValues): - self._parent.publish_attribute_value_changed(key, None) + self.attribute_value_changed(key, None) return self._data.pop(key) value_item = self._data[key] @@ -293,7 +293,7 @@ class PublishAttributes: output = value_item.data_to_store() # Reset values value_item.reset_values() - self._parent.publish_attribute_value_changed( + self.attribute_value_changed( key, value_item.data_to_store() ) return output diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9d9d13fda6..a00688a3fc 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -612,12 +612,12 @@ class CreateModel: self._on_create_instance_change() def set_instances_create_attr_values(self, instance_ids, key, value): - # TODO set bulk change context - for instance_id in instance_ids: - instance = self._get_instance_by_id(instance_id) - creator_attributes = instance["creator_attributes"] - if key in creator_attributes: - creator_attributes[key] = value + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id in instance_ids: + instance = self._get_instance_by_id(instance_id) + creator_attributes = instance["creator_attributes"] + if key in creator_attributes: + creator_attributes[key] = value def get_creator_attribute_definitions( self, instance_ids: List[str] @@ -658,14 +658,14 @@ class CreateModel: def set_instances_publish_attr_values( self, instance_ids, plugin_name, key, value ): - # TODO set bulk change context - for instance_id in instance_ids: - if instance_id is None: - instance = self._create_context - else: - instance = self._get_instance_by_id(instance_id) - plugin_val = instance.publish_attributes[plugin_name] - plugin_val[key] = value + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id in instance_ids: + if instance_id is None: + instance = self._create_context + else: + instance = self._get_instance_by_id(instance_id) + plugin_val = instance.publish_attributes[plugin_name] + plugin_val[key] = value def get_publish_attribute_definitions( self, @@ -792,7 +792,9 @@ class CreateModel: """Reset create instances.""" self._create_context.reset_context_data() - with self._create_context.bulk_instances_collection(): + with self._create_context.bulk_instances_collection( + CREATE_EVENT_SOURCE + ): try: self._create_context.reset_instances() except CreatorsOperationFailed as exc: From bad93d35324ab66a70ef3dbcfd23d8c165bfabea Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:40:41 +0200 Subject: [PATCH 045/546] use constants for topics --- client/ayon_core/pipeline/create/context.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 921ff9d2ad..ec859c3743 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -69,6 +69,12 @@ from .structures import ( UpdateData = collections.namedtuple("UpdateData", ["instance", "changes"]) _NOT_SET = object() +INSTANCE_ADDED_TOPIC = "instances.added" +INSTANCE_REMOVED_TOPIC = "instances.removed" +VALUE_CHANGED_TOPIC = "values.changed" +CREATE_ATTR_DEFS_CHANGED_TOPIC = "create.attr.defs.changed" +PUBLISH_ATTR_DEFS_CHANGED_TOPIC = "publish.attr.defs.changed" + def prepare_failed_convertor_operation_info(identifier, exc_info): exc_type, exc_value, exc_traceback = exc_info @@ -1091,7 +1097,7 @@ class CreateContext: self.get_instances_context_info(instances_to_validate) self._emit_event( - "instances.added", + INSTANCE_ADDED_TOPIC, { "instances": instances_to_validate, "create_context": self, @@ -1175,7 +1181,7 @@ class CreateContext: item_values.update(item_changes) self._emit_event( - "values.changed", + VALUE_CHANGED_TOPIC, event_data, sender ) @@ -1187,7 +1193,7 @@ class CreateContext: return self._emit_event( - "create.attr.defs.changed", + CREATE_ATTR_DEFS_CHANGED_TOPIC, { "instance_ids": set(data) }, @@ -1207,7 +1213,7 @@ class CreateContext: instance_ids.add(instance_id) self._emit_event( - "publish.attr.defs.changed", + PUBLISH_ATTR_DEFS_CHANGED_TOPIC, event_data, sender, ) @@ -1519,11 +1525,11 @@ class CreateContext: creator are just removed from context. Args: - instances(List[CreatedInstance]): Instances that should be removed. + instances (List[CreatedInstance]): Instances that should be removed. Remove logic is done using creator, which may require to do other cleanup than just remove instance from context. - """ + """ instances_by_identifier = collections.defaultdict(list) for instance in instances: identifier = instance.creator_identifier @@ -1675,7 +1681,7 @@ class CreateContext: return self._emit_event( - "instances.removed", + INSTANCE_REMOVED_TOPIC, { "instances": removed_instances, "create_context": self, From 813afc164fd99f09fe7e9267f539238c6d03c127 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:41:06 +0200 Subject: [PATCH 046/546] allow to pass sender to some methods --- client/ayon_core/pipeline/create/context.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index ec859c3743..fc2218dee6 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1518,7 +1518,7 @@ class CreateContext: if failed_info: raise CreatorsSaveFailed(failed_info) - def remove_instances(self, instances): + def remove_instances(self, instances, sender=None): """Remove instances from context. All instances that don't have creator identifier leading to existing @@ -1528,6 +1528,7 @@ class CreateContext: instances (List[CreatedInstance]): Instances that should be removed. Remove logic is done using creator, which may require to do other cleanup than just remove instance from context. + sender (Optional[str]): Sender of the event. """ instances_by_identifier = collections.defaultdict(list) @@ -1544,7 +1545,7 @@ class CreateContext: for instance in instances_by_identifier[identifier] ) - self._remove_instances(instances) + self._remove_instances(instances, sender) error_message = "Instances removement of creator \"{}\" failed. {}" failed_info = [] @@ -1670,7 +1671,7 @@ class CreateContext: data.setdefault("create_context", self) return self._event_hub.emit(topic, data, sender) - def _remove_instances(self, instances): + def _remove_instances(self, instances, sender=None): removed_instances = [] for instance in instances: obj = self._instances_by_id.pop(instance.id, None) @@ -1685,7 +1686,8 @@ class CreateContext: { "instances": removed_instances, "create_context": self, - } + }, + sender, ) def _create_with_unified_error( From f19c2d46c033e385d35e358a25f3ae3b0a4810bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:25:45 +0200 Subject: [PATCH 047/546] added helper methods to register listening to events --- client/ayon_core/pipeline/create/context.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index fc2218dee6..776edee30e 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -783,6 +783,23 @@ class CreateContext: plugin.__name__, attr_defs ) + def listen_to_added_instances(self, callback): + self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback) + + def listen_to_removed_instances(self, callback): + self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback) + + def listen_to_value_changes(self, callback): + self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback) + + def listen_to_create_attr_defs_change(self, callback): + self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback) + + def listen_to_publish_attr_defs_change(self, callback): + self._event_hub.add_callback( + PUBLISH_ATTR_DEFS_CHANGED_TOPIC, callback + ) + def context_data_to_store(self): """Data that should be stored by host function. From fd575a41809087d29f906c1b9c55745bd9fc76be Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Sep 2024 15:06:28 +0200 Subject: [PATCH 048/546] refresh instances based on create context events --- .../tools/publisher/models/create.py | 88 ++++++++++++++++--- .../publisher/widgets/overview_widget.py | 31 +++++-- 2 files changed, 99 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index a00688a3fc..08fd984721 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -362,6 +362,25 @@ class CreateModel: self._creator_items = None self._reset_instances() + + self._emit_event("create.model.reset") + + self._create_context.listen_to_added_instances( + self._cc_added_instance + ) + self._create_context.listen_to_removed_instances( + self._cc_removed_instance + ) + self._create_context.listen_to_value_changes( + self._cc_value_changed + ) + self._create_context.listen_to_create_attr_defs_change( + self._cc_create_attr_changed + ) + self._create_context.listen_to_publish_attr_defs_change( + self._cc_publish_attr_changed + ) + self._create_context.reset_finalization() def get_creator_items(self) -> Dict[str, CreatorItem]: @@ -521,7 +540,6 @@ class CreateModel: } ) - self._on_create_instance_change() return success def trigger_convertor_items(self, convertor_identifiers: List[str]): @@ -609,8 +627,6 @@ class CreateModel: # is not required. self._remove_instances_from_context(instance_ids) - self._on_create_instance_change() - def set_instances_create_attr_values(self, instance_ids, key, value): with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): for instance_id in instance_ids: @@ -792,9 +808,7 @@ class CreateModel: """Reset create instances.""" self._create_context.reset_context_data() - with self._create_context.bulk_instances_collection( - CREATE_EVENT_SOURCE - ): + with self._create_context.bulk_instances_collection(): try: self._create_context.reset_instances() except CreatorsOperationFailed as exc: @@ -829,8 +843,6 @@ class CreateModel: } ) - self._on_create_instance_change() - def _remove_instances_from_context(self, instance_ids: List[str]): instances_by_id = self._create_context.instances_by_id instances = [ @@ -848,9 +860,6 @@ class CreateModel: } ) - def _on_create_instance_change(self): - self._emit_event("instances.refresh.finished") - def _collect_creator_items(self) -> Dict[str, CreatorItem]: # TODO add crashed initialization of create plugins to report output = {} @@ -872,6 +881,63 @@ class CreateModel: return output + def _cc_added_instance(self, event): + instance_ids = { + instance.id + for instance in event.data["instances"] + } + self._emit_event( + "create.context.added.instance", + {"instance_ids": instance_ids}, + ) + + def _cc_removed_instance(self, event): + instance_ids = { + instance.id + for instance in event.data["instances"] + } + self._emit_event( + "create.context.removed.instance", + {"instance_ids": instance_ids}, + ) + + def _cc_value_changed(self, event): + if event.source != CREATE_EVENT_SOURCE: + return + + instance_ids = { + item["instance"].id + for item in event.data["changes"] + } + self._emit_event( + "create.context.value.changed", + {"instance_ids": instance_ids}, + ) + + def _cc_create_attr_changed(self, event): + if event.source != CREATE_EVENT_SOURCE: + return + instance_ids = { + instance.id + for instance in event.data["instances"] + } + self._emit_event( + "create.context.create.attrs.changed", + {"instance_ids": instance_ids}, + ) + + def _cc_publish_attr_changed(self, event): + if event.source != CREATE_EVENT_SOURCE: + return + event_data = { + instance_id: instance_data["plugin_names"] + for instance_id, instance_data in event.data.items() + } + self._emit_event( + "create.context.publish.attrs.changed", + event_data, + ) + def _get_allowed_creators_pattern(self) -> Union[Pattern, None]: """Provide regex pattern for configured creator labels in this context diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index d00edb9883..8ddd9bad7b 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -152,7 +152,16 @@ class OverviewWidget(QtWidgets.QFrame): "publish.reset.finished", self._on_publish_reset ) controller.register_event_callback( - "instances.refresh.finished", self._on_instances_refresh + "create.model.reset", + self._on_create_model_reset + ) + controller.register_event_callback( + "create.context.added.instance", + self._on_instances_added + ) + controller.register_event_callback( + "create.context.removed.instance", + self._on_instances_removed ) self._product_content_widget = product_content_widget @@ -436,6 +445,12 @@ class OverviewWidget(QtWidgets.QFrame): # Force to change instance and refresh details self._on_product_change() + # Give a change to process Resize Request + QtWidgets.QApplication.processEvents() + # Trigger update geometry of + widget = self._product_views_layout.currentWidget() + widget.updateGeometry() + def _on_publish_start(self): """Publish started.""" @@ -461,13 +476,11 @@ class OverviewWidget(QtWidgets.QFrame): self._controller.is_host_valid() ) - def _on_instances_refresh(self): - """Controller refreshed instances.""" - + def _on_create_model_reset(self): self._refresh_instances() - # Give a change to process Resize Request - QtWidgets.QApplication.processEvents() - # Trigger update geometry of - widget = self._product_views_layout.currentWidget() - widget.updateGeometry() + def _on_instances_added(self): + self._refresh_instances() + + def _on_instances_removed(self): + self._refresh_instances() From db0bc46a054240570989dfc763275bd219e2a8d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:05:17 +0200 Subject: [PATCH 049/546] modified event data and avoid unnecesasry triggers --- client/ayon_core/pipeline/create/context.py | 185 +++++++++++------- .../tools/publisher/models/create.py | 22 +-- 2 files changed, 127 insertions(+), 80 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 776edee30e..a6c42ef70a 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -132,6 +132,10 @@ class BulkInfo: def append(self, item): self._data.append(item) + def get_data(self): + """Use this method for read-only.""" + return self._data + def pop_data(self): data = self._data self._data = [] @@ -898,8 +902,8 @@ class CreateContext: Raises: CreatorError: If creator was not found or folder is empty. - """ + """ creator = self._get_creator_in_create(creator_identifier) project_name = self.project_name @@ -965,11 +969,12 @@ class CreateContext: active = bool(active) instance_data["active"] = active - return creator.create( - product_name, - instance_data, - _pre_create_data - ) + with self.bulk_instances_collection(): + return creator.create( + product_name, + instance_data, + _pre_create_data + ) def create_with_unified_error(self, identifier, *args, **kwargs): """Trigger create but raise only one error if anything fails. @@ -986,8 +991,8 @@ class CreateContext: CreatorsCreateFailed: When creation fails due to any possible reason. If anything goes wrong this is only possible exception the method should raise. - """ + """ result, fail_info = self._create_with_unified_error( identifier, None, *args, **kwargs ) @@ -1021,6 +1026,50 @@ class CreateContext: with self._bulk_context("collect", sender) as bulk_info: yield bulk_info + # Set publish attributes before bulk context is exited + for instance in bulk_info.get_data(): + publish_attributes = instance.publish_attributes + # Prepare publish plugin attributes and set it on instance + for plugin in self.plugins_with_defs: + try: + if is_func_signature_supported( + plugin.convert_attribute_values, self, instance + ): + plugin.convert_attribute_values(self, instance) + + elif plugin.__instanceEnabled__: + output = plugin.convert_attribute_values( + publish_attributes + ) + if output: + publish_attributes.update(output) + + except Exception: + self.log.error( + "Failed to convert attribute values of" + f" plugin '{plugin.__name__}'", + exc_info=True + ) + + for plugin in self.plugins_with_defs: + attr_defs = None + try: + attr_defs = plugin.get_attribute_defs_for_instance( + self, instance + ) + except Exception: + self.log.error( + "Failed to get attribute definitions" + f" from plugin '{plugin.__name__}'.", + exc_info=True + ) + + if not attr_defs: + continue + instance.set_publish_plugin_attr_defs( + plugin.__name__, attr_defs + ) + @contextmanager def bulk_value_changes(self, sender=None): with self._bulk_context("change", sender) as bulk_info: @@ -1037,21 +1086,38 @@ class CreateContext: yield bulk_info # --- instance change callbacks --- + def _is_instance_events_ready(self, instance_id): + # Context is ready + if instance_id is None: + return True + # Instance is not in yet in context + if instance_id not in self._instances_by_id: + return False + + # Instance in 'collect' bulk will be ignored + for instance in self._bulk_info["collect"].get_data(): + if instance.id == instance_id: + return False + return True + def instance_create_attr_defs_changed(self, instance_id): - with self.bulk_create_attr_defs_change() as bulk_item: - bulk_item.append(instance_id) + if self._is_instance_events_ready(instance_id): + with self.bulk_create_attr_defs_change() as bulk_item: + bulk_item.append(instance_id) def instance_publish_attr_defs_changed( self, instance_id, plugin_name ): - with self.bulk_publish_attr_defs_change() as bulk_item: - bulk_item.append((instance_id, plugin_name)) + if self._is_instance_events_ready(instance_id): + with self.bulk_publish_attr_defs_change() as bulk_item: + bulk_item.append((instance_id, plugin_name)) def instance_values_changed( self, instance_id, new_values ): - with self.bulk_value_changes() as bulk_item: - bulk_item.append((instance_id, new_values)) + if self._is_instance_events_ready(instance_id): + with self.bulk_value_changes() as bulk_item: + bulk_item.append((instance_id, new_values)) # --- context change callbacks --- def publish_attribute_value_changed(self, plugin_name, value): @@ -1117,54 +1183,10 @@ class CreateContext: INSTANCE_ADDED_TOPIC, { "instances": instances_to_validate, - "create_context": self, }, sender, ) - for instance in instances_to_validate: - publish_attributes = instance.publish_attributes - # Prepare publish plugin attributes and set it on instance - for plugin in self.plugins_with_defs: - try: - if is_func_signature_supported( - plugin.convert_attribute_values, self, instance - ): - plugin.convert_attribute_values(self, instance) - - elif plugin.__instanceEnabled__: - output = plugin.convert_attribute_values( - publish_attributes - ) - if output: - publish_attributes.update(output) - - except Exception: - self.log.error( - "Failed to convert attribute values of" - f" plugin '{plugin.__name__}'", - exc_info=True - ) - - for plugin in self.plugins_with_defs: - attr_defs = None - try: - attr_defs = plugin.get_attribute_defs_for_instance( - self, instance - ) - except Exception: - self.log.error( - "Failed to get attribute definitions" - f" from plugin '{plugin.__name__}'.", - exc_info=True - ) - - if not attr_defs: - continue - instance.set_publish_plugin_attr_defs( - plugin.__name__, attr_defs - ) - def _bulk_values_change( self, changes: Tuple[Union[str, None], Dict[str, Any]], @@ -1172,9 +1194,9 @@ class CreateContext: ): if not changes: return - event_data = {} + item_data_by_id = {} for item_id, item_changes in changes: - item_values = event_data.setdefault(item_id, {}) + item_values = item_data_by_id.setdefault(item_id, {}) if "creator_attributes" in item_changes: current_value = item_values.setdefault( "creator_attributes", {} @@ -1197,6 +1219,18 @@ class CreateContext: item_values.update(item_changes) + event_changes = [] + for item_id, item_changes in item_data_by_id.items(): + instance = self.get_instance_by_id(item_id) + event_changes.append({ + "instance": instance, + "changes": item_changes, + }) + + event_data = { + "changes": event_changes, + } + self._emit_event( VALUE_CHANGED_TOPIC, event_data, @@ -1204,15 +1238,19 @@ class CreateContext: ) def _bulk_create_attrs_change( - self, data: List[str], sender: Optional[str] + self, instance_ids: List[str], sender: Optional[str] ): - if not data: + if not instance_ids: return + instances = [ + self.get_instance_by_id(instance_id) + for instance_id in set(instance_ids) + ] self._emit_event( CREATE_ATTR_DEFS_CHANGED_TOPIC, { - "instance_ids": set(data) + "instances": instances, }, sender, ) @@ -1224,14 +1262,23 @@ class CreateContext: ): if not attr_info: return - event_data = {} - for plugin_name, instance_id in attr_info: - instance_ids = event_data.setdefault(plugin_name, set()) - instance_ids.add(instance_id) + + instance_changes = {} + for instance_id, plugin_name in attr_info: + instance_data = instance_changes.setdefault( + instance_id, + { + "instance": None, + "plugin_names": set(), + } + ) + instance = self.get_instance_by_id(instance_id) + instance_data["instance"] = instance + instance_data["plugin_names"].add(plugin_name) self._emit_event( PUBLISH_ATTR_DEFS_CHANGED_TOPIC, - event_data, + {"instance_changes": instance_changes}, sender, ) @@ -1702,7 +1749,6 @@ class CreateContext: INSTANCE_REMOVED_TOPIC, { "instances": removed_instances, - "create_context": self, }, sender, ) @@ -1726,7 +1772,8 @@ class CreateContext: label = getattr(creator, "label", label) # Run create - result = creator.create(*args, **kwargs) + with self.bulk_instances_collection(): + result = creator.create(*args, **kwargs) success = True except CreatorError: diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 08fd984721..f7e8aaa503 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -902,21 +902,22 @@ class CreateModel: ) def _cc_value_changed(self, event): - if event.source != CREATE_EVENT_SOURCE: + if event.source == CREATE_EVENT_SOURCE: return - instance_ids = { - item["instance"].id - for item in event.data["changes"] - } + instance_changes = {} + for item in event.data["changes"]: + instance_id = None + if item["instance"]: + instance_id = item["instance"].id + instance_changes[instance_id] = item["changes"] + self._emit_event( "create.context.value.changed", - {"instance_ids": instance_ids}, + {"instance_changes": instance_changes}, ) def _cc_create_attr_changed(self, event): - if event.source != CREATE_EVENT_SOURCE: - return instance_ids = { instance.id for instance in event.data["instances"] @@ -927,11 +928,10 @@ class CreateModel: ) def _cc_publish_attr_changed(self, event): - if event.source != CREATE_EVENT_SOURCE: - return + instance_changes = event.data["instance_changes"] event_data = { instance_id: instance_data["plugin_names"] - for instance_id, instance_data in event.data.items() + for instance_id, instance_data in instance_changes.items() } self._emit_event( "create.context.publish.attrs.changed", From 8a3cbb5f94069a8eabc2caac5027c55c48e45421 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:21:42 +0200 Subject: [PATCH 050/546] create plugin can register callbacks --- client/ayon_core/pipeline/create/creator_plugins.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 66725e7026..8d7ede1fa6 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -205,6 +205,7 @@ class BaseCreator(ABC): self.headless = headless self.apply_settings(project_settings) + self.register_callbacks() @staticmethod def _get_settings_values(project_settings, category_name, plugin_name): @@ -290,6 +291,14 @@ class BaseCreator(ABC): )) setattr(self, key, value) + def register_callbacks(self): + """Register callbacks for creator. + + Default implementation does nothing. It can be overridden to register + callbacks for creator. + """ + pass + @property def identifier(self): """Identifier of creator (must be unique). From d382343c12ff12efa06312b6f0fabeab5d2d3ae0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:22:53 +0200 Subject: [PATCH 051/546] update attr defs --- .../tools/publisher/widgets/widgets.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 429f7e0472..9fd687faf2 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1386,6 +1386,11 @@ class CreatorAttrsWidget(QtWidgets.QWidget): main_layout.setSpacing(0) main_layout.addWidget(scroll_area, 1) + controller.register_event_callback( + "create.context.create.attrs.changed", + self._on_instance_attr_defs_change + ) + self._main_layout = main_layout self._controller: AbstractPublisherFrontend = controller @@ -1393,6 +1398,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_instances = {} self._attr_def_id_to_attr_def = {} + self._current_instance_ids = set() # To store content of scroll area to prevent garbage collection self._content_widget = None @@ -1409,6 +1415,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): def set_current_instances(self, instance_ids): """Set current instances for which are attribute definitions shown.""" + self._current_instance_ids = set(instance_ids) prev_content_widget = self._scroll_area.widget() if prev_content_widget: self._scroll_area.takeWidget() @@ -1482,6 +1489,16 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._scroll_area.setWidget(content_widget) self._content_widget = content_widget + def _on_instance_attr_defs_change(self, event): + update = False + for instance_id in event.data["instance_ids"]: + if instance_id in self._current_instance_ids: + update = True + break + + if update: + self.set_current_instances(self._current_instance_ids) + def _input_value_changed(self, value, attr_id): instance_ids = self._attr_def_id_to_instances.get(attr_id) attr_def = self._attr_def_id_to_attr_def.get(attr_id) From e2ad2ce9c52e7cf72ca1f7724654c959ab79fd3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:31:04 +0200 Subject: [PATCH 052/546] autoconvert values --- client/ayon_core/pipeline/create/structures.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 79a1e61a55..9a89cb2e4d 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -113,7 +113,10 @@ class AttributeValues: self._data = {} for attr_def in attr_defs: value = values.get(attr_def.key) - if value is not None: + if value is None: + continue + converted_value = attr_def.convert_value(value) + if converted_value == value: self._data[attr_def.key] = value def __setitem__(self, key, value): @@ -339,13 +342,6 @@ class PublishAttributes: if value is None: value = {} - for attr_def in attr_defs: - if isinstance(attr_def, (UIDef, UnknownDef)): - continue - key = attr_def.key - if key in value: - value[key] = attr_def.convert_value(value[key]) - self._data[plugin_name] = PublishAttributeValues( self, plugin_name, attr_defs, value, value ) From 354bcf3122a194d458b523279cf37610199fddf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:31:16 +0200 Subject: [PATCH 053/546] fix 'update_create_attr_defs' --- client/ayon_core/pipeline/create/structures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 9a89cb2e4d..2bbd6dabc5 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -735,7 +735,7 @@ class CreatedInstance: return output def update_create_attr_defs(self, attr_defs, value=None): - if value is not None: + if value is None: value = self._data["creator_attributes"] if isinstance(value, AttributeValues): @@ -744,7 +744,7 @@ class CreatedInstance: if isinstance(self._data["creator_attributes"], AttributeValues): origin_data = self._data["creator_attributes"].origin_data else: - origin_data = self._data["creator_attributes"] + origin_data = copy.deepcopy(self._data["creator_attributes"]) self._data["creator_attributes"] = CreatorAttributeValues( self, "creator_attributes", From c8eb232461b3090acd6e3fb90f90b8225937b56d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:20:13 +0200 Subject: [PATCH 054/546] remove product widgets into more files --- .../publisher/widgets/overview_widget.py | 4 +- .../publisher/widgets/product_attributes.py | 325 ++++ .../publisher/widgets/product_context.py | 881 ++++++++++ .../tools/publisher/widgets/product_info.py | 284 ++++ .../tools/publisher/widgets/widgets.py | 1466 +---------------- 5 files changed, 1493 insertions(+), 1467 deletions(-) create mode 100644 client/ayon_core/tools/publisher/widgets/product_attributes.py create mode 100644 client/ayon_core/tools/publisher/widgets/product_context.py create mode 100644 client/ayon_core/tools/publisher/widgets/product_info.py diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index 8ddd9bad7b..beefa1ca98 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -6,12 +6,12 @@ from .border_label_widget import BorderedLabelWidget from .card_view_widgets import InstanceCardView from .list_view_widgets import InstanceListView from .widgets import ( - ProductAttributesWidget, CreateInstanceBtn, RemoveInstanceBtn, ChangeViewBtn, ) from .create_widget import CreateWidget +from .product_info import ProductInfoWidget class OverviewWidget(QtWidgets.QFrame): @@ -61,7 +61,7 @@ class OverviewWidget(QtWidgets.QFrame): product_attributes_wrap = BorderedLabelWidget( "Publish options", product_content_widget ) - product_attributes_widget = ProductAttributesWidget( + product_attributes_widget = ProductInfoWidget( controller, product_attributes_wrap ) product_attributes_wrap.set_center_widget(product_attributes_widget) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py new file mode 100644 index 0000000000..a696907a72 --- /dev/null +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -0,0 +1,325 @@ +from qtpy import QtWidgets, QtCore + +from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.tools.attribute_defs import create_widget_for_attr_def +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, +) + + +class CreatorAttrsWidget(QtWidgets.QWidget): + """Widget showing creator specific attributes for selected instances. + + Attributes are defined on creator so are dynamic. Their look and type is + based on attribute definitions that are defined in + `~/ayon_core/lib/attribute_definitions.py` and their widget + representation in `~/ayon_core/tools/attribute_defs/*`. + + Widgets are disabled if context of instance is not valid. + + Definitions are shown for all instance no matter if they are created with + different creators. If creator have same (similar) definitions their + widgets are merged into one (different label does not count). + """ + + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) + + scroll_area = QtWidgets.QScrollArea(self) + scroll_area.setWidgetResizable(True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(scroll_area, 1) + + controller.register_event_callback( + "create.context.create.attrs.changed", + self._on_instance_attr_defs_change + ) + + self._main_layout = main_layout + + self._controller: AbstractPublisherFrontend = controller + self._scroll_area = scroll_area + + self._attr_def_id_to_instances = {} + self._attr_def_id_to_attr_def = {} + self._current_instance_ids = set() + + # To store content of scroll area to prevent garbage collection + self._content_widget = None + + def set_instances_valid(self, valid): + """Change valid state of current instances.""" + + if ( + self._content_widget is not None + and self._content_widget.isEnabled() != valid + ): + self._content_widget.setEnabled(valid) + + def set_current_instances(self, instance_ids): + """Set current instances for which are attribute definitions shown.""" + + self._current_instance_ids = set(instance_ids) + prev_content_widget = self._scroll_area.widget() + if prev_content_widget: + self._scroll_area.takeWidget() + prev_content_widget.hide() + prev_content_widget.deleteLater() + + self._content_widget = None + self._attr_def_id_to_instances = {} + self._attr_def_id_to_attr_def = {} + + result = self._controller.get_creator_attribute_definitions( + instance_ids + ) + + content_widget = QtWidgets.QWidget(self._scroll_area) + content_layout = QtWidgets.QGridLayout(content_widget) + content_layout.setColumnStretch(0, 0) + content_layout.setColumnStretch(1, 1) + content_layout.setAlignment(QtCore.Qt.AlignTop) + content_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) + + row = 0 + for attr_def, instance_ids, values in result: + widget = create_widget_for_attr_def(attr_def, content_widget) + if attr_def.is_value_def: + if len(values) == 1: + value = values[0] + if value is not None: + widget.set_value(values[0]) + else: + widget.set_value(values, True) + + widget.value_changed.connect(self._input_value_changed) + self._attr_def_id_to_instances[attr_def.id] = instance_ids + self._attr_def_id_to_attr_def[attr_def.id] = attr_def + + if attr_def.hidden: + continue + + expand_cols = 2 + if attr_def.is_value_def and attr_def.is_label_horizontal: + expand_cols = 1 + + col_num = 2 - expand_cols + + label = None + if attr_def.is_value_def: + label = attr_def.label or attr_def.key + if label: + label_widget = QtWidgets.QLabel(label, self) + tooltip = attr_def.tooltip + if tooltip: + label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) + content_layout.addWidget( + label_widget, row, 0, 1, expand_cols + ) + if not attr_def.is_label_horizontal: + row += 1 + + content_layout.addWidget( + widget, row, col_num, 1, expand_cols + ) + row += 1 + + self._scroll_area.setWidget(content_widget) + self._content_widget = content_widget + + def _on_instance_attr_defs_change(self, event): + update = False + for instance_id in event.data["instance_ids"]: + if instance_id in self._current_instance_ids: + update = True + break + + if update: + self.set_current_instances(self._current_instance_ids) + + def _input_value_changed(self, value, attr_id): + instance_ids = self._attr_def_id_to_instances.get(attr_id) + attr_def = self._attr_def_id_to_attr_def.get(attr_id) + if not instance_ids or not attr_def: + return + self._controller.set_instances_create_attr_values( + instance_ids, attr_def.key, value + ) + + +class PublishPluginAttrsWidget(QtWidgets.QWidget): + """Widget showing publsish plugin attributes for selected instances. + + Attributes are defined on publish plugins. Publihs plugin may define + attribute definitions but must inherit `AYONPyblishPluginMixin` + (~/ayon_core/pipeline/publish). At the moment requires to implement + `get_attribute_defs` and `convert_attribute_values` class methods. + + Look and type of attributes is based on attribute definitions that are + defined in `~/ayon_core/lib/attribute_definitions.py` and their + widget representation in `~/ayon_core/tools/attribute_defs/*`. + + Widgets are disabled if context of instance is not valid. + + Definitions are shown for all instance no matter if they have different + product types. Similar definitions are merged into one (different label + does not count). + """ + + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) + + scroll_area = QtWidgets.QScrollArea(self) + scroll_area.setWidgetResizable(True) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + main_layout.addWidget(scroll_area, 1) + + self._main_layout = main_layout + + self._controller: AbstractPublisherFrontend = controller + self._scroll_area = scroll_area + + self._attr_def_id_to_instances = {} + self._attr_def_id_to_attr_def = {} + self._attr_def_id_to_plugin_name = {} + + # Store content of scroll area to prevent garbage collection + self._content_widget = None + + def set_instances_valid(self, valid): + """Change valid state of current instances.""" + if ( + self._content_widget is not None + and self._content_widget.isEnabled() != valid + ): + self._content_widget.setEnabled(valid) + + def set_current_instances(self, instance_ids, context_selected): + """Set current instances for which are attribute definitions shown.""" + + prev_content_widget = self._scroll_area.widget() + if prev_content_widget: + self._scroll_area.takeWidget() + prev_content_widget.hide() + prev_content_widget.deleteLater() + + self._content_widget = None + + self._attr_def_id_to_instances = {} + self._attr_def_id_to_attr_def = {} + self._attr_def_id_to_plugin_name = {} + + result = self._controller.get_publish_attribute_definitions( + instance_ids, context_selected + ) + + content_widget = QtWidgets.QWidget(self._scroll_area) + attr_def_widget = QtWidgets.QWidget(content_widget) + attr_def_layout = QtWidgets.QGridLayout(attr_def_widget) + attr_def_layout.setColumnStretch(0, 0) + attr_def_layout.setColumnStretch(1, 1) + attr_def_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + attr_def_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) + + content_layout = QtWidgets.QVBoxLayout(content_widget) + content_layout.addWidget(attr_def_widget, 0) + content_layout.addStretch(1) + + row = 0 + for plugin_name, attr_defs, all_plugin_values in result: + plugin_values = all_plugin_values[plugin_name] + + for attr_def in attr_defs: + widget = create_widget_for_attr_def( + attr_def, content_widget + ) + hidden_widget = attr_def.hidden + # Hide unknown values of publish plugins + # - The keys in most of cases does not represent what would + # label represent + if isinstance(attr_def, UnknownDef): + widget.setVisible(False) + hidden_widget = True + + if not hidden_widget: + expand_cols = 2 + if attr_def.is_value_def and attr_def.is_label_horizontal: + expand_cols = 1 + + col_num = 2 - expand_cols + label = None + if attr_def.is_value_def: + label = attr_def.label or attr_def.key + if label: + label_widget = QtWidgets.QLabel(label, content_widget) + tooltip = attr_def.tooltip + if tooltip: + label_widget.setToolTip(tooltip) + if attr_def.is_label_horizontal: + label_widget.setAlignment( + QtCore.Qt.AlignRight + | QtCore.Qt.AlignVCenter + ) + attr_def_layout.addWidget( + label_widget, row, 0, 1, expand_cols + ) + if not attr_def.is_label_horizontal: + row += 1 + attr_def_layout.addWidget( + widget, row, col_num, 1, expand_cols + ) + row += 1 + + if not attr_def.is_value_def: + continue + + widget.value_changed.connect(self._input_value_changed) + + attr_values = plugin_values[attr_def.key] + multivalue = len(attr_values) > 1 + values = [] + instances = [] + for instance, value in attr_values: + values.append(value) + instances.append(instance) + + self._attr_def_id_to_attr_def[attr_def.id] = attr_def + self._attr_def_id_to_instances[attr_def.id] = instances + self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name + + if multivalue: + widget.set_value(values, multivalue) + else: + widget.set_value(values[0]) + + self._scroll_area.setWidget(content_widget) + self._content_widget = content_widget + + def _input_value_changed(self, value, attr_id): + instance_ids = self._attr_def_id_to_instances.get(attr_id) + attr_def = self._attr_def_id_to_attr_def.get(attr_id) + plugin_name = self._attr_def_id_to_plugin_name.get(attr_id) + if not instance_ids or not attr_def or not plugin_name: + return + + self._controller.set_instances_publish_attr_values( + instance_ids, plugin_name, attr_def.key, value + ) diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py new file mode 100644 index 0000000000..b66d67717c --- /dev/null +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -0,0 +1,881 @@ +import re +import copy +import collections + +from qtpy import QtWidgets, QtCore, QtGui +import qtawesome + +from ayon_core.pipeline.create import ( + PRODUCT_NAME_ALLOWED_SYMBOLS, + TaskNotSetError, +) +from ayon_core.tools.utils import ( + PlaceholderLineEdit, + BaseClickableFrame, + set_style_property, +) +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend +from ayon_core.tools.publisher.constants import ( + VARIANT_TOOLTIP, + INPUTS_LAYOUT_HSPACING, + INPUTS_LAYOUT_VSPACING, +) + +from .folders_dialog import FoldersDialog +from .tasks_model import TasksModel +from .widgets import ClickableLineEdit, MultipleItemWidget + + +class FoldersFields(BaseClickableFrame): + """Field where folder path of selected instance/s is showed. + + Click on the field will trigger `FoldersDialog`. + """ + value_changed = QtCore.Signal() + + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) + self.setObjectName("FolderPathInputWidget") + + # Don't use 'self' for parent! + # - this widget has specific styles + dialog = FoldersDialog(controller, parent) + + name_input = ClickableLineEdit(self) + name_input.setObjectName("FolderPathInput") + + icon_name = "fa.window-maximize" + icon = qtawesome.icon(icon_name, color="white") + icon_btn = QtWidgets.QPushButton(self) + icon_btn.setIcon(icon) + icon_btn.setObjectName("FolderPathInputButton") + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(name_input, 1) + layout.addWidget(icon_btn, 0) + + # Make sure all widgets are vertically extended to highest widget + for widget in ( + name_input, + icon_btn + ): + size_policy = widget.sizePolicy() + size_policy.setVerticalPolicy( + QtWidgets.QSizePolicy.MinimumExpanding) + widget.setSizePolicy(size_policy) + name_input.clicked.connect(self._mouse_release_callback) + icon_btn.clicked.connect(self._mouse_release_callback) + dialog.finished.connect(self._on_dialog_finish) + + self._controller: AbstractPublisherFrontend = controller + self._dialog = dialog + self._name_input = name_input + self._icon_btn = icon_btn + + self._origin_value = [] + self._origin_selection = [] + self._selected_items = [] + self._has_value_changed = False + self._is_valid = True + self._multiselection_text = None + + def _on_dialog_finish(self, result): + if not result: + return + + folder_path = self._dialog.get_selected_folder_path() + if folder_path is None: + return + + self._selected_items = [folder_path] + self._has_value_changed = ( + self._origin_value != self._selected_items + ) + self.set_text(folder_path) + self._set_is_valid(True) + + self.value_changed.emit() + + def _mouse_release_callback(self): + self._dialog.set_selected_folders(self._selected_items) + self._dialog.open() + + def set_multiselection_text(self, text): + """Change text for multiselection of different folders. + + When there are selected multiple instances at once and they don't have + same folder in context. + """ + self._multiselection_text = text + + def _set_is_valid(self, valid): + if valid == self._is_valid: + return + self._is_valid = valid + state = "" + if not valid: + state = "invalid" + self._set_state_property(state) + + def _set_state_property(self, state): + set_style_property(self, "state", state) + set_style_property(self._name_input, "state", state) + set_style_property(self._icon_btn, "state", state) + + def is_valid(self): + """Is folder valid.""" + return self._is_valid + + def has_value_changed(self): + """Value of folder has changed.""" + return self._has_value_changed + + def get_selected_items(self): + """Selected folder paths.""" + return list(self._selected_items) + + def set_text(self, text): + """Set text in text field. + + Does not change selected items (folders). + """ + self._name_input.setText(text) + self._name_input.end(False) + + def set_selected_items(self, folder_paths=None): + """Set folder paths for selection of instances. + + Passed folder paths are validated and if there are 2 or more different + folder paths then multiselection text is shown. + + Args: + folder_paths (list, tuple, set, NoneType): List of folder paths. + + """ + if folder_paths is None: + folder_paths = [] + + self._has_value_changed = False + self._origin_value = list(folder_paths) + self._selected_items = list(folder_paths) + is_valid = self._controller.are_folder_paths_valid(folder_paths) + if not folder_paths: + self.set_text("") + + elif len(folder_paths) == 1: + folder_path = tuple(folder_paths)[0] + self.set_text(folder_path) + else: + multiselection_text = self._multiselection_text + if multiselection_text is None: + multiselection_text = "|".join(folder_paths) + self.set_text(multiselection_text) + + self._set_is_valid(is_valid) + + def reset_to_origin(self): + """Change to folder paths set with last `set_selected_items` call.""" + self.set_selected_items(self._origin_value) + + def confirm_value(self): + self._origin_value = copy.deepcopy(self._selected_items) + self._has_value_changed = False + + +class TasksComboboxProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._filter_empty = False + + def set_filter_empty(self, filter_empty): + if self._filter_empty is filter_empty: + return + self._filter_empty = filter_empty + self.invalidate() + + def filterAcceptsRow(self, source_row, parent_index): + if self._filter_empty: + model = self.sourceModel() + source_index = model.index( + source_row, self.filterKeyColumn(), parent_index + ) + if not source_index.data(QtCore.Qt.DisplayRole): + return False + return True + + +class TasksCombobox(QtWidgets.QComboBox): + """Combobox to show tasks for selected instances. + + Combobox gives ability to select only from intersection of task names for + folder paths in selected instances. + + If folder paths in selected instances does not have same tasks then combobox + will be empty. + """ + value_changed = QtCore.Signal() + + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) + self.setObjectName("TasksCombobox") + + # Set empty delegate to propagate stylesheet to a combobox + delegate = QtWidgets.QStyledItemDelegate() + self.setItemDelegate(delegate) + + model = TasksModel(controller, True) + proxy_model = TasksComboboxProxy() + proxy_model.setSourceModel(model) + self.setModel(proxy_model) + + self.currentIndexChanged.connect(self._on_index_change) + + self._delegate = delegate + self._model = model + self._proxy_model = proxy_model + self._origin_value = [] + self._origin_selection = [] + self._selected_items = [] + self._has_value_changed = False + self._ignore_index_change = False + self._multiselection_text = None + self._is_valid = True + + self._text = None + + # Make sure combobox is extended horizontally + size_policy = self.sizePolicy() + size_policy.setHorizontalPolicy( + QtWidgets.QSizePolicy.MinimumExpanding) + self.setSizePolicy(size_policy) + + def set_invalid_empty_task(self, invalid=True): + self._proxy_model.set_filter_empty(invalid) + if invalid: + self._set_is_valid(False) + self.set_text( + "< One or more products require Task selected >" + ) + else: + self.set_text(None) + + def set_multiselection_text(self, text): + """Change text shown when multiple different tasks are in context.""" + self._multiselection_text = text + + def _on_index_change(self): + if self._ignore_index_change: + return + + self.set_text(None) + text = self.currentText() + idx = self.findText(text) + if idx < 0: + return + + self._set_is_valid(True) + self._selected_items = [text] + self._has_value_changed = ( + self._origin_selection != self._selected_items + ) + + self.value_changed.emit() + + def set_text(self, text): + """Set context shown in combobox without changing selected items.""" + if text == self._text: + return + + self._text = text + self.repaint() + + def paintEvent(self, event): + """Paint custom text without using QLineEdit. + + The easiest way how to draw custom text in combobox and keep combobox + properties and event handling. + """ + painter = QtGui.QPainter(self) + painter.setPen(self.palette().color(QtGui.QPalette.Text)) + opt = QtWidgets.QStyleOptionComboBox() + self.initStyleOption(opt) + if self._text is not None: + opt.currentText = self._text + + style = self.style() + style.drawComplexControl( + QtWidgets.QStyle.CC_ComboBox, opt, painter, self + ) + style.drawControl( + QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self + ) + painter.end() + + def is_valid(self): + """Are all selected items valid.""" + return self._is_valid + + def has_value_changed(self): + """Did selection of task changed.""" + return self._has_value_changed + + def _set_is_valid(self, valid): + if valid == self._is_valid: + return + self._is_valid = valid + state = "" + if not valid: + state = "invalid" + self._set_state_property(state) + + def _set_state_property(self, state): + current_value = self.property("state") + if current_value != state: + self.setProperty("state", state) + self.style().polish(self) + + def get_selected_items(self): + """Get selected tasks. + + If value has changed then will return list with single item. + + Returns: + list: Selected tasks. + """ + return list(self._selected_items) + + def set_folder_paths(self, folder_paths): + """Set folder paths for which should show tasks.""" + self._ignore_index_change = True + + self._model.set_folder_paths(folder_paths) + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) + + self._ignore_index_change = False + + # It is a bug if not exactly one folder got here + if len(folder_paths) != 1: + self.set_selected_item("") + self._set_is_valid(False) + return + + folder_path = tuple(folder_paths)[0] + + is_valid = False + if self._selected_items: + is_valid = True + + valid_task_names = [] + for task_name in self._selected_items: + _is_valid = self._model.is_task_name_valid(folder_path, task_name) + if _is_valid: + valid_task_names.append(task_name) + else: + is_valid = _is_valid + + self._selected_items = valid_task_names + if len(self._selected_items) == 0: + self.set_selected_item("") + + elif len(self._selected_items) == 1: + self.set_selected_item(self._selected_items[0]) + + else: + multiselection_text = self._multiselection_text + if multiselection_text is None: + multiselection_text = "|".join(self._selected_items) + self.set_selected_item(multiselection_text) + + self._set_is_valid(is_valid) + + def confirm_value(self, folder_paths): + new_task_name = self._selected_items[0] + self._origin_value = [ + (folder_path, new_task_name) + for folder_path in folder_paths + ] + self._origin_selection = copy.deepcopy(self._selected_items) + self._has_value_changed = False + + def set_selected_items(self, folder_task_combinations=None): + """Set items for selected instances. + + Args: + folder_task_combinations (list): List of tuples. Each item in + the list contain folder path and task name. + """ + self._proxy_model.set_filter_empty(False) + self._proxy_model.sort(0) + + if folder_task_combinations is None: + folder_task_combinations = [] + + task_names = set() + task_names_by_folder_path = collections.defaultdict(set) + for folder_path, task_name in folder_task_combinations: + task_names.add(task_name) + task_names_by_folder_path[folder_path].add(task_name) + folder_paths = set(task_names_by_folder_path.keys()) + + self._ignore_index_change = True + + self._model.set_folder_paths(folder_paths) + + self._has_value_changed = False + + self._origin_value = copy.deepcopy(folder_task_combinations) + + self._origin_selection = list(task_names) + self._selected_items = list(task_names) + # Reset current index + self.setCurrentIndex(-1) + is_valid = True + if not task_names: + self.set_selected_item("") + + elif len(task_names) == 1: + task_name = tuple(task_names)[0] + idx = self.findText(task_name) + is_valid = not idx < 0 + if not is_valid and len(folder_paths) > 1: + is_valid = self._validate_task_names_by_folder_paths( + task_names_by_folder_path + ) + self.set_selected_item(task_name) + + else: + for task_name in task_names: + idx = self.findText(task_name) + is_valid = not idx < 0 + if not is_valid: + break + + if not is_valid and len(folder_paths) > 1: + is_valid = self._validate_task_names_by_folder_paths( + task_names_by_folder_path + ) + multiselection_text = self._multiselection_text + if multiselection_text is None: + multiselection_text = "|".join(task_names) + self.set_selected_item(multiselection_text) + + self._set_is_valid(is_valid) + + self._ignore_index_change = False + + self.value_changed.emit() + + def _validate_task_names_by_folder_paths(self, task_names_by_folder_path): + for folder_path, task_names in task_names_by_folder_path.items(): + for task_name in task_names: + if not self._model.is_task_name_valid(folder_path, task_name): + return False + return True + + def set_selected_item(self, item_name): + """Set task which is set on selected instance. + + Args: + item_name(str): Task name which should be selected. + """ + idx = self.findText(item_name) + # Set current index (must be set to -1 if is invalid) + self.setCurrentIndex(idx) + self.set_text(item_name) + + def reset_to_origin(self): + """Change to task names set with last `set_selected_items` call.""" + self.set_selected_items(self._origin_value) + + +class VariantInputWidget(PlaceholderLineEdit): + """Input widget for variant.""" + value_changed = QtCore.Signal() + + def __init__(self, parent): + super().__init__(parent) + + self.setObjectName("VariantInput") + self.setToolTip(VARIANT_TOOLTIP) + + name_pattern = "^[{}]*$".format(PRODUCT_NAME_ALLOWED_SYMBOLS) + self._name_pattern = name_pattern + self._compiled_name_pattern = re.compile(name_pattern) + + self._origin_value = [] + self._current_value = [] + + self._ignore_value_change = False + self._has_value_changed = False + self._multiselection_text = None + + self._is_valid = True + + self.textChanged.connect(self._on_text_change) + + def is_valid(self): + """Is variant text valid.""" + return self._is_valid + + def has_value_changed(self): + """Value of variant has changed.""" + return self._has_value_changed + + def _set_state_property(self, state): + current_value = self.property("state") + if current_value != state: + self.setProperty("state", state) + self.style().polish(self) + + def set_multiselection_text(self, text): + """Change text of multiselection.""" + self._multiselection_text = text + + def confirm_value(self): + self._origin_value = copy.deepcopy(self._current_value) + self._has_value_changed = False + + def _set_is_valid(self, valid): + if valid == self._is_valid: + return + self._is_valid = valid + state = "" + if not valid: + state = "invalid" + self._set_state_property(state) + + def _on_text_change(self): + if self._ignore_value_change: + return + + is_valid = bool(self._compiled_name_pattern.match(self.text())) + self._set_is_valid(is_valid) + + self._current_value = [self.text()] + self._has_value_changed = self._current_value != self._origin_value + + self.value_changed.emit() + + def reset_to_origin(self): + """Set origin value of selected instances.""" + self.set_value(self._origin_value) + + def get_value(self): + """Get current value. + + Origin value returned if didn't change. + """ + return copy.deepcopy(self._current_value) + + def set_value(self, variants=None): + """Set value of currently selected instances.""" + if variants is None: + variants = [] + + self._ignore_value_change = True + + self._has_value_changed = False + + self._origin_value = list(variants) + self._current_value = list(variants) + + self.setPlaceholderText("") + if not variants: + self.setText("") + + elif len(variants) == 1: + self.setText(self._current_value[0]) + + else: + multiselection_text = self._multiselection_text + if multiselection_text is None: + multiselection_text = "|".join(variants) + self.setText("") + self.setPlaceholderText(multiselection_text) + + self._ignore_value_change = False + + +class GlobalAttrsWidget(QtWidgets.QWidget): + """Global attributes mainly to define context and product name of instances. + + product name is or may be affected on context. Gives abiity to modify + context and product name of instance. This change is not autopromoted but + must be submitted. + + Warning: Until artist hit `Submit` changes must not be propagated to + instance data. + + Global attributes contain these widgets: + Variant: [ text input ] + Folder: [ folder dialog ] + Task: [ combobox ] + Product type: [ immutable ] + product name: [ immutable ] + [Submit] [Cancel] + """ + instance_context_changed = QtCore.Signal() + + multiselection_text = "< Multiselection >" + unknown_value = "N/A" + + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) + + self._controller: AbstractPublisherFrontend = controller + self._current_instances = [] + + variant_input = VariantInputWidget(self) + folder_value_widget = FoldersFields(controller, self) + task_value_widget = TasksCombobox(controller, self) + product_type_value_widget = MultipleItemWidget(self) + product_value_widget = MultipleItemWidget(self) + + variant_input.set_multiselection_text(self.multiselection_text) + folder_value_widget.set_multiselection_text(self.multiselection_text) + task_value_widget.set_multiselection_text(self.multiselection_text) + + variant_input.set_value() + folder_value_widget.set_selected_items() + task_value_widget.set_selected_items() + product_type_value_widget.set_value() + product_value_widget.set_value() + + submit_btn = QtWidgets.QPushButton("Confirm", self) + cancel_btn = QtWidgets.QPushButton("Cancel", self) + submit_btn.setEnabled(False) + cancel_btn.setEnabled(False) + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.setSpacing(5) + btns_layout.addWidget(submit_btn) + btns_layout.addWidget(cancel_btn) + + main_layout = QtWidgets.QFormLayout(self) + main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) + main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) + main_layout.addRow("Variant", variant_input) + main_layout.addRow("Folder", folder_value_widget) + main_layout.addRow("Task", task_value_widget) + main_layout.addRow("Product type", product_type_value_widget) + main_layout.addRow("Product name", product_value_widget) + main_layout.addRow(btns_layout) + + variant_input.value_changed.connect(self._on_variant_change) + folder_value_widget.value_changed.connect(self._on_folder_change) + task_value_widget.value_changed.connect(self._on_task_change) + submit_btn.clicked.connect(self._on_submit) + cancel_btn.clicked.connect(self._on_cancel) + + self.variant_input = variant_input + self.folder_value_widget = folder_value_widget + self.task_value_widget = task_value_widget + self.product_type_value_widget = product_type_value_widget + self.product_value_widget = product_value_widget + self.submit_btn = submit_btn + self.cancel_btn = cancel_btn + + def _on_submit(self): + """Commit changes for selected instances.""" + + variant_value = None + folder_path = None + task_name = None + if self.variant_input.has_value_changed(): + variant_value = self.variant_input.get_value()[0] + + if self.folder_value_widget.has_value_changed(): + folder_path = self.folder_value_widget.get_selected_items()[0] + + if self.task_value_widget.has_value_changed(): + task_name = self.task_value_widget.get_selected_items()[0] + + product_names = set() + invalid_tasks = False + folder_paths = [] + for instance in self._current_instances: + # Ignore instances that have promised context + if instance.has_promised_context: + continue + + new_variant_value = instance.variant + new_folder_path = instance.folder_path + new_task_name = instance.task_name + if variant_value is not None: + new_variant_value = variant_value + + if folder_path is not None: + new_folder_path = folder_path + + if task_name is not None: + new_task_name = task_name + + folder_paths.append(new_folder_path) + try: + new_product_name = self._controller.get_product_name( + instance.creator_identifier, + new_variant_value, + new_task_name, + new_folder_path, + instance.id, + ) + + except TaskNotSetError: + invalid_tasks = True + product_names.add(instance.product_name) + continue + + product_names.add(new_product_name) + if variant_value is not None: + instance.variant = variant_value + + if folder_path is not None: + instance.folder_path = folder_path + + if task_name is not None: + instance.task_name = task_name or None + + instance.product_name = new_product_name + + if invalid_tasks: + self.task_value_widget.set_invalid_empty_task() + + self.product_value_widget.set_value(product_names) + + self._set_btns_enabled(False) + self._set_btns_visible(invalid_tasks) + + if variant_value is not None: + self.variant_input.confirm_value() + + if folder_path is not None: + self.folder_value_widget.confirm_value() + + if task_name is not None: + self.task_value_widget.confirm_value(folder_paths) + + self.instance_context_changed.emit() + + def _on_cancel(self): + """Cancel changes and set back to their irigin value.""" + + self.variant_input.reset_to_origin() + self.folder_value_widget.reset_to_origin() + self.task_value_widget.reset_to_origin() + self._set_btns_enabled(False) + + def _on_value_change(self): + any_invalid = ( + not self.variant_input.is_valid() + or not self.folder_value_widget.is_valid() + or not self.task_value_widget.is_valid() + ) + any_changed = ( + self.variant_input.has_value_changed() + or self.folder_value_widget.has_value_changed() + or self.task_value_widget.has_value_changed() + ) + self._set_btns_visible(any_changed or any_invalid) + self.cancel_btn.setEnabled(any_changed) + self.submit_btn.setEnabled(not any_invalid) + + def _on_variant_change(self): + self._on_value_change() + + def _on_folder_change(self): + folder_paths = self.folder_value_widget.get_selected_items() + self.task_value_widget.set_folder_paths(folder_paths) + self._on_value_change() + + def _on_task_change(self): + self._on_value_change() + + def _set_btns_visible(self, visible): + self.cancel_btn.setVisible(visible) + self.submit_btn.setVisible(visible) + + def _set_btns_enabled(self, enabled): + self.cancel_btn.setEnabled(enabled) + self.submit_btn.setEnabled(enabled) + + def set_current_instances(self, instances): + """Set currently selected instances. + + Args: + instances (List[InstanceItem]): List of selected instances. + Empty instances tells that nothing or context is selected. + """ + self._set_btns_visible(False) + + self._current_instances = instances + + folder_paths = set() + variants = set() + product_types = set() + product_names = set() + + editable = True + if len(instances) == 0: + editable = False + + folder_task_combinations = [] + context_editable = None + for instance in instances: + if not instance.has_promised_context: + context_editable = True + elif context_editable is None: + context_editable = False + + # NOTE I'm not sure how this can even happen? + if instance.creator_identifier is None: + editable = False + + variants.add(instance.variant or self.unknown_value) + product_types.add(instance.product_type or self.unknown_value) + folder_path = instance.folder_path or self.unknown_value + task_name = instance.task_name or "" + folder_paths.add(folder_path) + folder_task_combinations.append((folder_path, task_name)) + product_names.add(instance.product_name or self.unknown_value) + + if not editable: + context_editable = False + elif context_editable is None: + context_editable = True + + self.variant_input.set_value(variants) + + # Set context of folder widget + self.folder_value_widget.set_selected_items(folder_paths) + # Set context of task widget + self.task_value_widget.set_selected_items(folder_task_combinations) + self.product_type_value_widget.set_value(product_types) + self.product_value_widget.set_value(product_names) + + self.variant_input.setEnabled(editable) + self.folder_value_widget.setEnabled(context_editable) + self.task_value_widget.setEnabled(context_editable) + + if not editable: + folder_tooltip = "Select instances to change folder path." + task_tooltip = "Select instances to change task name." + elif not context_editable: + folder_tooltip = "Folder path is defined by Create plugin." + task_tooltip = "Task is defined by Create plugin." + else: + folder_tooltip = "Change folder path of selected instances." + task_tooltip = "Change task of selected instances." + + self.folder_value_widget.setToolTip(folder_tooltip) + self.task_value_widget.setToolTip(task_tooltip) diff --git a/client/ayon_core/tools/publisher/widgets/product_info.py b/client/ayon_core/tools/publisher/widgets/product_info.py new file mode 100644 index 0000000000..c1d2037bd3 --- /dev/null +++ b/client/ayon_core/tools/publisher/widgets/product_info.py @@ -0,0 +1,284 @@ +import os +import uuid +import shutil + +from qtpy import QtWidgets, QtCore + +from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend + +from .thumbnail_widget import ThumbnailWidget +from .product_context import GlobalAttrsWidget +from .product_attributes import ( + CreatorAttrsWidget, + PublishPluginAttrsWidget, +) + + +class ProductInfoWidget(QtWidgets.QWidget): + """Wrapper widget where attributes of instance/s are modified. + ┌─────────────────┬─────────────┐ + │ Global │ │ + │ attributes │ Thumbnail │ TOP + │ │ │ + ├─────────────┬───┴─────────────┤ + │ Creator │ Publish │ + │ attributes │ plugin │ BOTTOM + │ │ attributes │ + └───────────────────────────────┘ + """ + instance_context_changed = QtCore.Signal() + convert_requested = QtCore.Signal() + + def __init__( + self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget + ): + super().__init__(parent) + + # TOP PART + top_widget = QtWidgets.QWidget(self) + + # Global attributes + global_attrs_widget = GlobalAttrsWidget(controller, top_widget) + thumbnail_widget = ThumbnailWidget(controller, top_widget) + + top_layout = QtWidgets.QHBoxLayout(top_widget) + top_layout.setContentsMargins(0, 0, 0, 0) + top_layout.addWidget(global_attrs_widget, 7) + top_layout.addWidget(thumbnail_widget, 3) + + # BOTTOM PART + bottom_widget = QtWidgets.QWidget(self) + + # Wrap Creator attributes to widget to be able add convert button + creator_widget = QtWidgets.QWidget(bottom_widget) + + # Convert button widget (with layout to handle stretch) + convert_widget = QtWidgets.QWidget(creator_widget) + convert_label = QtWidgets.QLabel(creator_widget) + # Set the label text with 'setText' to apply html + convert_label.setText( + ( + "Found old publishable products" + " incompatible with new publisher." + "

Press the update products button" + " to automatically update them" + " to be able to publish again." + ) + ) + convert_label.setWordWrap(True) + convert_label.setAlignment(QtCore.Qt.AlignCenter) + + convert_btn = QtWidgets.QPushButton( + "Update products", convert_widget + ) + convert_separator = QtWidgets.QFrame(convert_widget) + convert_separator.setObjectName("Separator") + convert_separator.setMinimumHeight(1) + convert_separator.setMaximumHeight(1) + + convert_layout = QtWidgets.QGridLayout(convert_widget) + convert_layout.setContentsMargins(5, 0, 5, 0) + convert_layout.setVerticalSpacing(10) + convert_layout.addWidget(convert_label, 0, 0, 1, 3) + convert_layout.addWidget(convert_btn, 1, 1) + convert_layout.addWidget(convert_separator, 2, 0, 1, 3) + convert_layout.setColumnStretch(0, 1) + convert_layout.setColumnStretch(1, 0) + convert_layout.setColumnStretch(2, 1) + + # Creator attributes widget + creator_attrs_widget = CreatorAttrsWidget( + controller, creator_widget + ) + creator_layout = QtWidgets.QVBoxLayout(creator_widget) + creator_layout.setContentsMargins(0, 0, 0, 0) + creator_layout.addWidget(convert_widget, 0) + creator_layout.addWidget(creator_attrs_widget, 1) + + publish_attrs_widget = PublishPluginAttrsWidget( + controller, bottom_widget + ) + + bottom_separator = QtWidgets.QWidget(bottom_widget) + bottom_separator.setObjectName("Separator") + bottom_separator.setMinimumWidth(1) + + bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) + bottom_layout.setContentsMargins(0, 0, 0, 0) + bottom_layout.addWidget(creator_widget, 1) + bottom_layout.addWidget(bottom_separator, 0) + bottom_layout.addWidget(publish_attrs_widget, 1) + + top_bottom = QtWidgets.QWidget(self) + top_bottom.setObjectName("Separator") + top_bottom.setMinimumHeight(1) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(top_widget, 0) + layout.addWidget(top_bottom, 0) + layout.addWidget(bottom_widget, 1) + + self._convertor_identifiers = None + self._current_instances = None + self._context_selected = False + self._all_instances_valid = True + + global_attrs_widget.instance_context_changed.connect( + self._on_instance_context_changed + ) + convert_btn.clicked.connect(self._on_convert_click) + thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) + thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) + + controller.register_event_callback( + "instance.thumbnail.changed", self._on_thumbnail_changed + ) + + self._controller: AbstractPublisherFrontend = controller + + self._convert_widget = convert_widget + + self.global_attrs_widget = global_attrs_widget + + self.creator_attrs_widget = creator_attrs_widget + self.publish_attrs_widget = publish_attrs_widget + self._thumbnail_widget = thumbnail_widget + + self.top_bottom = top_bottom + self.bottom_separator = bottom_separator + + def _on_instance_context_changed(self): + instance_ids = { + instance.id + for instance in self._current_instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) + all_valid = True + for instance_id, context_info in context_info_by_id.items(): + if not context_info.is_valid: + all_valid = False + break + + self._all_instances_valid = all_valid + self.creator_attrs_widget.set_instances_valid(all_valid) + self.publish_attrs_widget.set_instances_valid(all_valid) + + self.instance_context_changed.emit() + + def _on_convert_click(self): + self.convert_requested.emit() + + def set_current_instances( + self, instances, context_selected, convertor_identifiers + ): + """Change currently selected items. + + Args: + instances (List[InstanceItem]): List of currently selected + instances. + context_selected (bool): Is context selected. + convertor_identifiers (List[str]): Identifiers of convert items. + """ + + instance_ids = { + instance.id + for instance in instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) + + all_valid = True + for context_info in context_info_by_id.values(): + if not context_info.is_valid: + all_valid = False + break + + s_convertor_identifiers = set(convertor_identifiers) + self._convertor_identifiers = s_convertor_identifiers + self._current_instances = instances + self._context_selected = context_selected + self._all_instances_valid = all_valid + + self._convert_widget.setVisible(len(s_convertor_identifiers) > 0) + self.global_attrs_widget.set_current_instances(instances) + self.creator_attrs_widget.set_current_instances(instance_ids) + self.publish_attrs_widget.set_current_instances( + instance_ids, context_selected + ) + self.creator_attrs_widget.set_instances_valid(all_valid) + self.publish_attrs_widget.set_instances_valid(all_valid) + + self._update_thumbnails() + + def _on_thumbnail_create(self, path): + instance_ids = [ + instance.id + for instance in self._current_instances + ] + if self._context_selected: + instance_ids.append(None) + + if not instance_ids: + return + + mapping = {} + if len(instance_ids) == 1: + mapping[instance_ids[0]] = path + + else: + for instance_id in instance_ids: + root = os.path.dirname(path) + ext = os.path.splitext(path)[-1] + dst_path = os.path.join(root, str(uuid.uuid4()) + ext) + shutil.copy(path, dst_path) + mapping[instance_id] = dst_path + + self._controller.set_thumbnail_paths_for_instances(mapping) + + def _on_thumbnail_clear(self): + instance_ids = [ + instance.id + for instance in self._current_instances + ] + if self._context_selected: + instance_ids.append(None) + + if not instance_ids: + return + + mapping = { + instance_id: None + for instance_id in instance_ids + } + self._controller.set_thumbnail_paths_for_instances(mapping) + + def _on_thumbnail_changed(self, event): + self._update_thumbnails() + + def _update_thumbnails(self): + instance_ids = [ + instance.id + for instance in self._current_instances + ] + if self._context_selected: + instance_ids.append(None) + + if not instance_ids: + self._thumbnail_widget.setVisible(False) + self._thumbnail_widget.set_current_thumbnails(None) + return + + mapping = self._controller.get_thumbnail_paths_for_instances( + instance_ids + ) + thumbnail_paths = [] + for instance_id in instance_ids: + path = mapping[instance_id] + if path: + thumbnail_paths.append(path) + + self._thumbnail_widget.setVisible(True) + self._thumbnail_widget.set_current_thumbnails(thumbnail_paths) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 9fd687faf2..00c87ac249 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1,41 +1,18 @@ # -*- coding: utf-8 -*- import os -import re -import copy import functools -import uuid -import shutil -import collections from qtpy import QtWidgets, QtCore, QtGui import qtawesome -from ayon_core.lib.attribute_definitions import UnknownDef from ayon_core.style import get_objected_colors -from ayon_core.pipeline.create import ( - PRODUCT_NAME_ALLOWED_SYMBOLS, - TaskNotSetError, -) -from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools import resources from ayon_core.tools.flickcharm import FlickCharm from ayon_core.tools.utils import ( - PlaceholderLineEdit, IconButton, PixmapLabel, - BaseClickableFrame, - set_style_property, -) -from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend -from ayon_core.tools.publisher.constants import ( - VARIANT_TOOLTIP, - ResetKeySequence, - INPUTS_LAYOUT_HSPACING, - INPUTS_LAYOUT_VSPACING, ) +from ayon_core.tools.publisher.constants import ResetKeySequence -from .thumbnail_widget import ThumbnailWidget -from .folders_dialog import FoldersDialog -from .tasks_model import TasksModel from .icons import ( get_pixmap, get_icon_path @@ -426,583 +403,6 @@ class ClickableLineEdit(QtWidgets.QLineEdit): event.accept() -class FoldersFields(BaseClickableFrame): - """Field where folder path of selected instance/s is showed. - - Click on the field will trigger `FoldersDialog`. - """ - value_changed = QtCore.Signal() - - def __init__( - self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget - ): - super().__init__(parent) - self.setObjectName("FolderPathInputWidget") - - # Don't use 'self' for parent! - # - this widget has specific styles - dialog = FoldersDialog(controller, parent) - - name_input = ClickableLineEdit(self) - name_input.setObjectName("FolderPathInput") - - icon_name = "fa.window-maximize" - icon = qtawesome.icon(icon_name, color="white") - icon_btn = QtWidgets.QPushButton(self) - icon_btn.setIcon(icon) - icon_btn.setObjectName("FolderPathInputButton") - - layout = QtWidgets.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(name_input, 1) - layout.addWidget(icon_btn, 0) - - # Make sure all widgets are vertically extended to highest widget - for widget in ( - name_input, - icon_btn - ): - size_policy = widget.sizePolicy() - size_policy.setVerticalPolicy( - QtWidgets.QSizePolicy.MinimumExpanding) - widget.setSizePolicy(size_policy) - name_input.clicked.connect(self._mouse_release_callback) - icon_btn.clicked.connect(self._mouse_release_callback) - dialog.finished.connect(self._on_dialog_finish) - - self._controller: AbstractPublisherFrontend = controller - self._dialog = dialog - self._name_input = name_input - self._icon_btn = icon_btn - - self._origin_value = [] - self._origin_selection = [] - self._selected_items = [] - self._has_value_changed = False - self._is_valid = True - self._multiselection_text = None - - def _on_dialog_finish(self, result): - if not result: - return - - folder_path = self._dialog.get_selected_folder_path() - if folder_path is None: - return - - self._selected_items = [folder_path] - self._has_value_changed = ( - self._origin_value != self._selected_items - ) - self.set_text(folder_path) - self._set_is_valid(True) - - self.value_changed.emit() - - def _mouse_release_callback(self): - self._dialog.set_selected_folders(self._selected_items) - self._dialog.open() - - def set_multiselection_text(self, text): - """Change text for multiselection of different folders. - - When there are selected multiple instances at once and they don't have - same folder in context. - """ - self._multiselection_text = text - - def _set_is_valid(self, valid): - if valid == self._is_valid: - return - self._is_valid = valid - state = "" - if not valid: - state = "invalid" - self._set_state_property(state) - - def _set_state_property(self, state): - set_style_property(self, "state", state) - set_style_property(self._name_input, "state", state) - set_style_property(self._icon_btn, "state", state) - - def is_valid(self): - """Is folder valid.""" - return self._is_valid - - def has_value_changed(self): - """Value of folder has changed.""" - return self._has_value_changed - - def get_selected_items(self): - """Selected folder paths.""" - return list(self._selected_items) - - def set_text(self, text): - """Set text in text field. - - Does not change selected items (folders). - """ - self._name_input.setText(text) - self._name_input.end(False) - - def set_selected_items(self, folder_paths=None): - """Set folder paths for selection of instances. - - Passed folder paths are validated and if there are 2 or more different - folder paths then multiselection text is shown. - - Args: - folder_paths (list, tuple, set, NoneType): List of folder paths. - - """ - if folder_paths is None: - folder_paths = [] - - self._has_value_changed = False - self._origin_value = list(folder_paths) - self._selected_items = list(folder_paths) - is_valid = self._controller.are_folder_paths_valid(folder_paths) - if not folder_paths: - self.set_text("") - - elif len(folder_paths) == 1: - folder_path = tuple(folder_paths)[0] - self.set_text(folder_path) - else: - multiselection_text = self._multiselection_text - if multiselection_text is None: - multiselection_text = "|".join(folder_paths) - self.set_text(multiselection_text) - - self._set_is_valid(is_valid) - - def reset_to_origin(self): - """Change to folder paths set with last `set_selected_items` call.""" - self.set_selected_items(self._origin_value) - - def confirm_value(self): - self._origin_value = copy.deepcopy(self._selected_items) - self._has_value_changed = False - - -class TasksComboboxProxy(QtCore.QSortFilterProxyModel): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._filter_empty = False - - def set_filter_empty(self, filter_empty): - if self._filter_empty is filter_empty: - return - self._filter_empty = filter_empty - self.invalidate() - - def filterAcceptsRow(self, source_row, parent_index): - if self._filter_empty: - model = self.sourceModel() - source_index = model.index( - source_row, self.filterKeyColumn(), parent_index - ) - if not source_index.data(QtCore.Qt.DisplayRole): - return False - return True - - -class TasksCombobox(QtWidgets.QComboBox): - """Combobox to show tasks for selected instances. - - Combobox gives ability to select only from intersection of task names for - folder paths in selected instances. - - If folder paths in selected instances does not have same tasks then combobox - will be empty. - """ - value_changed = QtCore.Signal() - - def __init__( - self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget - ): - super().__init__(parent) - self.setObjectName("TasksCombobox") - - # Set empty delegate to propagate stylesheet to a combobox - delegate = QtWidgets.QStyledItemDelegate() - self.setItemDelegate(delegate) - - model = TasksModel(controller, True) - proxy_model = TasksComboboxProxy() - proxy_model.setSourceModel(model) - self.setModel(proxy_model) - - self.currentIndexChanged.connect(self._on_index_change) - - self._delegate = delegate - self._model = model - self._proxy_model = proxy_model - self._origin_value = [] - self._origin_selection = [] - self._selected_items = [] - self._has_value_changed = False - self._ignore_index_change = False - self._multiselection_text = None - self._is_valid = True - - self._text = None - - # Make sure combobox is extended horizontally - size_policy = self.sizePolicy() - size_policy.setHorizontalPolicy( - QtWidgets.QSizePolicy.MinimumExpanding) - self.setSizePolicy(size_policy) - - def set_invalid_empty_task(self, invalid=True): - self._proxy_model.set_filter_empty(invalid) - if invalid: - self._set_is_valid(False) - self.set_text( - "< One or more products require Task selected >" - ) - else: - self.set_text(None) - - def set_multiselection_text(self, text): - """Change text shown when multiple different tasks are in context.""" - self._multiselection_text = text - - def _on_index_change(self): - if self._ignore_index_change: - return - - self.set_text(None) - text = self.currentText() - idx = self.findText(text) - if idx < 0: - return - - self._set_is_valid(True) - self._selected_items = [text] - self._has_value_changed = ( - self._origin_selection != self._selected_items - ) - - self.value_changed.emit() - - def set_text(self, text): - """Set context shown in combobox without changing selected items.""" - if text == self._text: - return - - self._text = text - self.repaint() - - def paintEvent(self, event): - """Paint custom text without using QLineEdit. - - The easiest way how to draw custom text in combobox and keep combobox - properties and event handling. - """ - painter = QtGui.QPainter(self) - painter.setPen(self.palette().color(QtGui.QPalette.Text)) - opt = QtWidgets.QStyleOptionComboBox() - self.initStyleOption(opt) - if self._text is not None: - opt.currentText = self._text - - style = self.style() - style.drawComplexControl( - QtWidgets.QStyle.CC_ComboBox, opt, painter, self - ) - style.drawControl( - QtWidgets.QStyle.CE_ComboBoxLabel, opt, painter, self - ) - painter.end() - - def is_valid(self): - """Are all selected items valid.""" - return self._is_valid - - def has_value_changed(self): - """Did selection of task changed.""" - return self._has_value_changed - - def _set_is_valid(self, valid): - if valid == self._is_valid: - return - self._is_valid = valid - state = "" - if not valid: - state = "invalid" - self._set_state_property(state) - - def _set_state_property(self, state): - current_value = self.property("state") - if current_value != state: - self.setProperty("state", state) - self.style().polish(self) - - def get_selected_items(self): - """Get selected tasks. - - If value has changed then will return list with single item. - - Returns: - list: Selected tasks. - """ - return list(self._selected_items) - - def set_folder_paths(self, folder_paths): - """Set folder paths for which should show tasks.""" - self._ignore_index_change = True - - self._model.set_folder_paths(folder_paths) - self._proxy_model.set_filter_empty(False) - self._proxy_model.sort(0) - - self._ignore_index_change = False - - # It is a bug if not exactly one folder got here - if len(folder_paths) != 1: - self.set_selected_item("") - self._set_is_valid(False) - return - - folder_path = tuple(folder_paths)[0] - - is_valid = False - if self._selected_items: - is_valid = True - - valid_task_names = [] - for task_name in self._selected_items: - _is_valid = self._model.is_task_name_valid(folder_path, task_name) - if _is_valid: - valid_task_names.append(task_name) - else: - is_valid = _is_valid - - self._selected_items = valid_task_names - if len(self._selected_items) == 0: - self.set_selected_item("") - - elif len(self._selected_items) == 1: - self.set_selected_item(self._selected_items[0]) - - else: - multiselection_text = self._multiselection_text - if multiselection_text is None: - multiselection_text = "|".join(self._selected_items) - self.set_selected_item(multiselection_text) - - self._set_is_valid(is_valid) - - def confirm_value(self, folder_paths): - new_task_name = self._selected_items[0] - self._origin_value = [ - (folder_path, new_task_name) - for folder_path in folder_paths - ] - self._origin_selection = copy.deepcopy(self._selected_items) - self._has_value_changed = False - - def set_selected_items(self, folder_task_combinations=None): - """Set items for selected instances. - - Args: - folder_task_combinations (list): List of tuples. Each item in - the list contain folder path and task name. - """ - self._proxy_model.set_filter_empty(False) - self._proxy_model.sort(0) - - if folder_task_combinations is None: - folder_task_combinations = [] - - task_names = set() - task_names_by_folder_path = collections.defaultdict(set) - for folder_path, task_name in folder_task_combinations: - task_names.add(task_name) - task_names_by_folder_path[folder_path].add(task_name) - folder_paths = set(task_names_by_folder_path.keys()) - - self._ignore_index_change = True - - self._model.set_folder_paths(folder_paths) - - self._has_value_changed = False - - self._origin_value = copy.deepcopy(folder_task_combinations) - - self._origin_selection = list(task_names) - self._selected_items = list(task_names) - # Reset current index - self.setCurrentIndex(-1) - is_valid = True - if not task_names: - self.set_selected_item("") - - elif len(task_names) == 1: - task_name = tuple(task_names)[0] - idx = self.findText(task_name) - is_valid = not idx < 0 - if not is_valid and len(folder_paths) > 1: - is_valid = self._validate_task_names_by_folder_paths( - task_names_by_folder_path - ) - self.set_selected_item(task_name) - - else: - for task_name in task_names: - idx = self.findText(task_name) - is_valid = not idx < 0 - if not is_valid: - break - - if not is_valid and len(folder_paths) > 1: - is_valid = self._validate_task_names_by_folder_paths( - task_names_by_folder_path - ) - multiselection_text = self._multiselection_text - if multiselection_text is None: - multiselection_text = "|".join(task_names) - self.set_selected_item(multiselection_text) - - self._set_is_valid(is_valid) - - self._ignore_index_change = False - - self.value_changed.emit() - - def _validate_task_names_by_folder_paths(self, task_names_by_folder_path): - for folder_path, task_names in task_names_by_folder_path.items(): - for task_name in task_names: - if not self._model.is_task_name_valid(folder_path, task_name): - return False - return True - - def set_selected_item(self, item_name): - """Set task which is set on selected instance. - - Args: - item_name(str): Task name which should be selected. - """ - idx = self.findText(item_name) - # Set current index (must be set to -1 if is invalid) - self.setCurrentIndex(idx) - self.set_text(item_name) - - def reset_to_origin(self): - """Change to task names set with last `set_selected_items` call.""" - self.set_selected_items(self._origin_value) - - -class VariantInputWidget(PlaceholderLineEdit): - """Input widget for variant.""" - value_changed = QtCore.Signal() - - def __init__(self, parent): - super().__init__(parent) - - self.setObjectName("VariantInput") - self.setToolTip(VARIANT_TOOLTIP) - - name_pattern = "^[{}]*$".format(PRODUCT_NAME_ALLOWED_SYMBOLS) - self._name_pattern = name_pattern - self._compiled_name_pattern = re.compile(name_pattern) - - self._origin_value = [] - self._current_value = [] - - self._ignore_value_change = False - self._has_value_changed = False - self._multiselection_text = None - - self._is_valid = True - - self.textChanged.connect(self._on_text_change) - - def is_valid(self): - """Is variant text valid.""" - return self._is_valid - - def has_value_changed(self): - """Value of variant has changed.""" - return self._has_value_changed - - def _set_state_property(self, state): - current_value = self.property("state") - if current_value != state: - self.setProperty("state", state) - self.style().polish(self) - - def set_multiselection_text(self, text): - """Change text of multiselection.""" - self._multiselection_text = text - - def confirm_value(self): - self._origin_value = copy.deepcopy(self._current_value) - self._has_value_changed = False - - def _set_is_valid(self, valid): - if valid == self._is_valid: - return - self._is_valid = valid - state = "" - if not valid: - state = "invalid" - self._set_state_property(state) - - def _on_text_change(self): - if self._ignore_value_change: - return - - is_valid = bool(self._compiled_name_pattern.match(self.text())) - self._set_is_valid(is_valid) - - self._current_value = [self.text()] - self._has_value_changed = self._current_value != self._origin_value - - self.value_changed.emit() - - def reset_to_origin(self): - """Set origin value of selected instances.""" - self.set_value(self._origin_value) - - def get_value(self): - """Get current value. - - Origin value returned if didn't change. - """ - return copy.deepcopy(self._current_value) - - def set_value(self, variants=None): - """Set value of currently selected instances.""" - if variants is None: - variants = [] - - self._ignore_value_change = True - - self._has_value_changed = False - - self._origin_value = list(variants) - self._current_value = list(variants) - - self.setPlaceholderText("") - if not variants: - self.setText("") - - elif len(variants) == 1: - self.setText(self._current_value[0]) - - else: - multiselection_text = self._multiselection_text - if multiselection_text is None: - multiselection_text = "|".join(variants) - self.setText("") - self.setPlaceholderText(multiselection_text) - - self._ignore_value_change = False - - class MultipleItemWidget(QtWidgets.QWidget): """Widget for immutable text which can have more than one value. @@ -1080,870 +480,6 @@ class MultipleItemWidget(QtWidgets.QWidget): self._model.appendRow(item) -class GlobalAttrsWidget(QtWidgets.QWidget): - """Global attributes mainly to define context and product name of instances. - - product name is or may be affected on context. Gives abiity to modify - context and product name of instance. This change is not autopromoted but - must be submitted. - - Warning: Until artist hit `Submit` changes must not be propagated to - instance data. - - Global attributes contain these widgets: - Variant: [ text input ] - Folder: [ folder dialog ] - Task: [ combobox ] - Product type: [ immutable ] - product name: [ immutable ] - [Submit] [Cancel] - """ - instance_context_changed = QtCore.Signal() - - multiselection_text = "< Multiselection >" - unknown_value = "N/A" - - def __init__( - self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget - ): - super().__init__(parent) - - self._controller: AbstractPublisherFrontend = controller - self._current_instances = [] - - variant_input = VariantInputWidget(self) - folder_value_widget = FoldersFields(controller, self) - task_value_widget = TasksCombobox(controller, self) - product_type_value_widget = MultipleItemWidget(self) - product_value_widget = MultipleItemWidget(self) - - variant_input.set_multiselection_text(self.multiselection_text) - folder_value_widget.set_multiselection_text(self.multiselection_text) - task_value_widget.set_multiselection_text(self.multiselection_text) - - variant_input.set_value() - folder_value_widget.set_selected_items() - task_value_widget.set_selected_items() - product_type_value_widget.set_value() - product_value_widget.set_value() - - submit_btn = QtWidgets.QPushButton("Confirm", self) - cancel_btn = QtWidgets.QPushButton("Cancel", self) - submit_btn.setEnabled(False) - cancel_btn.setEnabled(False) - - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addStretch(1) - btns_layout.setSpacing(5) - btns_layout.addWidget(submit_btn) - btns_layout.addWidget(cancel_btn) - - main_layout = QtWidgets.QFormLayout(self) - main_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) - main_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) - main_layout.addRow("Variant", variant_input) - main_layout.addRow("Folder", folder_value_widget) - main_layout.addRow("Task", task_value_widget) - main_layout.addRow("Product type", product_type_value_widget) - main_layout.addRow("Product name", product_value_widget) - main_layout.addRow(btns_layout) - - variant_input.value_changed.connect(self._on_variant_change) - folder_value_widget.value_changed.connect(self._on_folder_change) - task_value_widget.value_changed.connect(self._on_task_change) - submit_btn.clicked.connect(self._on_submit) - cancel_btn.clicked.connect(self._on_cancel) - - self.variant_input = variant_input - self.folder_value_widget = folder_value_widget - self.task_value_widget = task_value_widget - self.product_type_value_widget = product_type_value_widget - self.product_value_widget = product_value_widget - self.submit_btn = submit_btn - self.cancel_btn = cancel_btn - - def _on_submit(self): - """Commit changes for selected instances.""" - - variant_value = None - folder_path = None - task_name = None - if self.variant_input.has_value_changed(): - variant_value = self.variant_input.get_value()[0] - - if self.folder_value_widget.has_value_changed(): - folder_path = self.folder_value_widget.get_selected_items()[0] - - if self.task_value_widget.has_value_changed(): - task_name = self.task_value_widget.get_selected_items()[0] - - product_names = set() - invalid_tasks = False - folder_paths = [] - for instance in self._current_instances: - # Ignore instances that have promised context - if instance.has_promised_context: - continue - - new_variant_value = instance.variant - new_folder_path = instance.folder_path - new_task_name = instance.task_name - if variant_value is not None: - new_variant_value = variant_value - - if folder_path is not None: - new_folder_path = folder_path - - if task_name is not None: - new_task_name = task_name - - folder_paths.append(new_folder_path) - try: - new_product_name = self._controller.get_product_name( - instance.creator_identifier, - new_variant_value, - new_task_name, - new_folder_path, - instance.id, - ) - - except TaskNotSetError: - invalid_tasks = True - product_names.add(instance.product_name) - continue - - product_names.add(new_product_name) - if variant_value is not None: - instance.variant = variant_value - - if folder_path is not None: - instance.folder_path = folder_path - - if task_name is not None: - instance.task_name = task_name or None - - instance.product_name = new_product_name - - if invalid_tasks: - self.task_value_widget.set_invalid_empty_task() - - self.product_value_widget.set_value(product_names) - - self._set_btns_enabled(False) - self._set_btns_visible(invalid_tasks) - - if variant_value is not None: - self.variant_input.confirm_value() - - if folder_path is not None: - self.folder_value_widget.confirm_value() - - if task_name is not None: - self.task_value_widget.confirm_value(folder_paths) - - self.instance_context_changed.emit() - - def _on_cancel(self): - """Cancel changes and set back to their irigin value.""" - - self.variant_input.reset_to_origin() - self.folder_value_widget.reset_to_origin() - self.task_value_widget.reset_to_origin() - self._set_btns_enabled(False) - - def _on_value_change(self): - any_invalid = ( - not self.variant_input.is_valid() - or not self.folder_value_widget.is_valid() - or not self.task_value_widget.is_valid() - ) - any_changed = ( - self.variant_input.has_value_changed() - or self.folder_value_widget.has_value_changed() - or self.task_value_widget.has_value_changed() - ) - self._set_btns_visible(any_changed or any_invalid) - self.cancel_btn.setEnabled(any_changed) - self.submit_btn.setEnabled(not any_invalid) - - def _on_variant_change(self): - self._on_value_change() - - def _on_folder_change(self): - folder_paths = self.folder_value_widget.get_selected_items() - self.task_value_widget.set_folder_paths(folder_paths) - self._on_value_change() - - def _on_task_change(self): - self._on_value_change() - - def _set_btns_visible(self, visible): - self.cancel_btn.setVisible(visible) - self.submit_btn.setVisible(visible) - - def _set_btns_enabled(self, enabled): - self.cancel_btn.setEnabled(enabled) - self.submit_btn.setEnabled(enabled) - - def set_current_instances(self, instances): - """Set currently selected instances. - - Args: - instances (List[InstanceItem]): List of selected instances. - Empty instances tells that nothing or context is selected. - """ - self._set_btns_visible(False) - - self._current_instances = instances - - folder_paths = set() - variants = set() - product_types = set() - product_names = set() - - editable = True - if len(instances) == 0: - editable = False - - folder_task_combinations = [] - context_editable = None - for instance in instances: - if not instance.has_promised_context: - context_editable = True - elif context_editable is None: - context_editable = False - - # NOTE I'm not sure how this can even happen? - if instance.creator_identifier is None: - editable = False - - variants.add(instance.variant or self.unknown_value) - product_types.add(instance.product_type or self.unknown_value) - folder_path = instance.folder_path or self.unknown_value - task_name = instance.task_name or "" - folder_paths.add(folder_path) - folder_task_combinations.append((folder_path, task_name)) - product_names.add(instance.product_name or self.unknown_value) - - if not editable: - context_editable = False - elif context_editable is None: - context_editable = True - - self.variant_input.set_value(variants) - - # Set context of folder widget - self.folder_value_widget.set_selected_items(folder_paths) - # Set context of task widget - self.task_value_widget.set_selected_items(folder_task_combinations) - self.product_type_value_widget.set_value(product_types) - self.product_value_widget.set_value(product_names) - - self.variant_input.setEnabled(editable) - self.folder_value_widget.setEnabled(context_editable) - self.task_value_widget.setEnabled(context_editable) - - if not editable: - folder_tooltip = "Select instances to change folder path." - task_tooltip = "Select instances to change task name." - elif not context_editable: - folder_tooltip = "Folder path is defined by Create plugin." - task_tooltip = "Task is defined by Create plugin." - else: - folder_tooltip = "Change folder path of selected instances." - task_tooltip = "Change task of selected instances." - - self.folder_value_widget.setToolTip(folder_tooltip) - self.task_value_widget.setToolTip(task_tooltip) - - -class CreatorAttrsWidget(QtWidgets.QWidget): - """Widget showing creator specific attributes for selected instances. - - Attributes are defined on creator so are dynamic. Their look and type is - based on attribute definitions that are defined in - `~/ayon_core/lib/attribute_definitions.py` and their widget - representation in `~/ayon_core/tools/attribute_defs/*`. - - Widgets are disabled if context of instance is not valid. - - Definitions are shown for all instance no matter if they are created with - different creators. If creator have same (similar) definitions their - widgets are merged into one (different label does not count). - """ - - def __init__( - self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget - ): - super().__init__(parent) - - scroll_area = QtWidgets.QScrollArea(self) - scroll_area.setWidgetResizable(True) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - main_layout.addWidget(scroll_area, 1) - - controller.register_event_callback( - "create.context.create.attrs.changed", - self._on_instance_attr_defs_change - ) - - self._main_layout = main_layout - - self._controller: AbstractPublisherFrontend = controller - self._scroll_area = scroll_area - - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._current_instance_ids = set() - - # To store content of scroll area to prevent garbage collection - self._content_widget = None - - def set_instances_valid(self, valid): - """Change valid state of current instances.""" - - if ( - self._content_widget is not None - and self._content_widget.isEnabled() != valid - ): - self._content_widget.setEnabled(valid) - - def set_current_instances(self, instance_ids): - """Set current instances for which are attribute definitions shown.""" - - self._current_instance_ids = set(instance_ids) - prev_content_widget = self._scroll_area.widget() - if prev_content_widget: - self._scroll_area.takeWidget() - prev_content_widget.hide() - prev_content_widget.deleteLater() - - self._content_widget = None - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - - result = self._controller.get_creator_attribute_definitions( - instance_ids - ) - - content_widget = QtWidgets.QWidget(self._scroll_area) - content_layout = QtWidgets.QGridLayout(content_widget) - content_layout.setColumnStretch(0, 0) - content_layout.setColumnStretch(1, 1) - content_layout.setAlignment(QtCore.Qt.AlignTop) - content_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) - content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) - - row = 0 - for attr_def, instance_ids, values in result: - widget = create_widget_for_attr_def(attr_def, content_widget) - if attr_def.is_value_def: - if len(values) == 1: - value = values[0] - if value is not None: - widget.set_value(values[0]) - else: - widget.set_value(values, True) - - widget.value_changed.connect(self._input_value_changed) - self._attr_def_id_to_instances[attr_def.id] = instance_ids - self._attr_def_id_to_attr_def[attr_def.id] = attr_def - - if attr_def.hidden: - continue - - expand_cols = 2 - if attr_def.is_value_def and attr_def.is_label_horizontal: - expand_cols = 1 - - col_num = 2 - expand_cols - - label = None - if attr_def.is_value_def: - label = attr_def.label or attr_def.key - if label: - label_widget = QtWidgets.QLabel(label, self) - tooltip = attr_def.tooltip - if tooltip: - label_widget.setToolTip(tooltip) - if attr_def.is_label_horizontal: - label_widget.setAlignment( - QtCore.Qt.AlignRight - | QtCore.Qt.AlignVCenter - ) - content_layout.addWidget( - label_widget, row, 0, 1, expand_cols - ) - if not attr_def.is_label_horizontal: - row += 1 - - content_layout.addWidget( - widget, row, col_num, 1, expand_cols - ) - row += 1 - - self._scroll_area.setWidget(content_widget) - self._content_widget = content_widget - - def _on_instance_attr_defs_change(self, event): - update = False - for instance_id in event.data["instance_ids"]: - if instance_id in self._current_instance_ids: - update = True - break - - if update: - self.set_current_instances(self._current_instance_ids) - - def _input_value_changed(self, value, attr_id): - instance_ids = self._attr_def_id_to_instances.get(attr_id) - attr_def = self._attr_def_id_to_attr_def.get(attr_id) - if not instance_ids or not attr_def: - return - self._controller.set_instances_create_attr_values( - instance_ids, attr_def.key, value - ) - - -class PublishPluginAttrsWidget(QtWidgets.QWidget): - """Widget showing publsish plugin attributes for selected instances. - - Attributes are defined on publish plugins. Publihs plugin may define - attribute definitions but must inherit `AYONPyblishPluginMixin` - (~/ayon_core/pipeline/publish). At the moment requires to implement - `get_attribute_defs` and `convert_attribute_values` class methods. - - Look and type of attributes is based on attribute definitions that are - defined in `~/ayon_core/lib/attribute_definitions.py` and their - widget representation in `~/ayon_core/tools/attribute_defs/*`. - - Widgets are disabled if context of instance is not valid. - - Definitions are shown for all instance no matter if they have different - product types. Similar definitions are merged into one (different label - does not count). - """ - - def __init__( - self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget - ): - super().__init__(parent) - - scroll_area = QtWidgets.QScrollArea(self) - scroll_area.setWidgetResizable(True) - - main_layout = QtWidgets.QHBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - main_layout.addWidget(scroll_area, 1) - - self._main_layout = main_layout - - self._controller: AbstractPublisherFrontend = controller - self._scroll_area = scroll_area - - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._attr_def_id_to_plugin_name = {} - - # Store content of scroll area to prevent garbage collection - self._content_widget = None - - def set_instances_valid(self, valid): - """Change valid state of current instances.""" - if ( - self._content_widget is not None - and self._content_widget.isEnabled() != valid - ): - self._content_widget.setEnabled(valid) - - def set_current_instances(self, instance_ids, context_selected): - """Set current instances for which are attribute definitions shown.""" - - prev_content_widget = self._scroll_area.widget() - if prev_content_widget: - self._scroll_area.takeWidget() - prev_content_widget.hide() - prev_content_widget.deleteLater() - - self._content_widget = None - - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._attr_def_id_to_plugin_name = {} - - result = self._controller.get_publish_attribute_definitions( - instance_ids, context_selected - ) - - content_widget = QtWidgets.QWidget(self._scroll_area) - attr_def_widget = QtWidgets.QWidget(content_widget) - attr_def_layout = QtWidgets.QGridLayout(attr_def_widget) - attr_def_layout.setColumnStretch(0, 0) - attr_def_layout.setColumnStretch(1, 1) - attr_def_layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) - attr_def_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) - - content_layout = QtWidgets.QVBoxLayout(content_widget) - content_layout.addWidget(attr_def_widget, 0) - content_layout.addStretch(1) - - row = 0 - for plugin_name, attr_defs, all_plugin_values in result: - plugin_values = all_plugin_values[plugin_name] - - for attr_def in attr_defs: - widget = create_widget_for_attr_def( - attr_def, content_widget - ) - hidden_widget = attr_def.hidden - # Hide unknown values of publish plugins - # - The keys in most of cases does not represent what would - # label represent - if isinstance(attr_def, UnknownDef): - widget.setVisible(False) - hidden_widget = True - - if not hidden_widget: - expand_cols = 2 - if attr_def.is_value_def and attr_def.is_label_horizontal: - expand_cols = 1 - - col_num = 2 - expand_cols - label = None - if attr_def.is_value_def: - label = attr_def.label or attr_def.key - if label: - label_widget = QtWidgets.QLabel(label, content_widget) - tooltip = attr_def.tooltip - if tooltip: - label_widget.setToolTip(tooltip) - if attr_def.is_label_horizontal: - label_widget.setAlignment( - QtCore.Qt.AlignRight - | QtCore.Qt.AlignVCenter - ) - attr_def_layout.addWidget( - label_widget, row, 0, 1, expand_cols - ) - if not attr_def.is_label_horizontal: - row += 1 - attr_def_layout.addWidget( - widget, row, col_num, 1, expand_cols - ) - row += 1 - - if not attr_def.is_value_def: - continue - - widget.value_changed.connect(self._input_value_changed) - - attr_values = plugin_values[attr_def.key] - multivalue = len(attr_values) > 1 - values = [] - instances = [] - for instance, value in attr_values: - values.append(value) - instances.append(instance) - - self._attr_def_id_to_attr_def[attr_def.id] = attr_def - self._attr_def_id_to_instances[attr_def.id] = instances - self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name - - if multivalue: - widget.set_value(values, multivalue) - else: - widget.set_value(values[0]) - - self._scroll_area.setWidget(content_widget) - self._content_widget = content_widget - - def _input_value_changed(self, value, attr_id): - instance_ids = self._attr_def_id_to_instances.get(attr_id) - attr_def = self._attr_def_id_to_attr_def.get(attr_id) - plugin_name = self._attr_def_id_to_plugin_name.get(attr_id) - if not instance_ids or not attr_def or not plugin_name: - return - - self._controller.set_instances_publish_attr_values( - instance_ids, plugin_name, attr_def.key, value - ) - - -class ProductAttributesWidget(QtWidgets.QWidget): - """Wrapper widget where attributes of instance/s are modified. - ┌─────────────────┬─────────────┐ - │ Global │ │ - │ attributes │ Thumbnail │ TOP - │ │ │ - ├─────────────┬───┴─────────────┤ - │ Creator │ Publish │ - │ attributes │ plugin │ BOTTOM - │ │ attributes │ - └───────────────────────────────┘ - """ - instance_context_changed = QtCore.Signal() - convert_requested = QtCore.Signal() - - def __init__( - self, controller: AbstractPublisherFrontend, parent: QtWidgets.QWidget - ): - super().__init__(parent) - - # TOP PART - top_widget = QtWidgets.QWidget(self) - - # Global attributes - global_attrs_widget = GlobalAttrsWidget(controller, top_widget) - thumbnail_widget = ThumbnailWidget(controller, top_widget) - - top_layout = QtWidgets.QHBoxLayout(top_widget) - top_layout.setContentsMargins(0, 0, 0, 0) - top_layout.addWidget(global_attrs_widget, 7) - top_layout.addWidget(thumbnail_widget, 3) - - # BOTTOM PART - bottom_widget = QtWidgets.QWidget(self) - - # Wrap Creator attributes to widget to be able add convert button - creator_widget = QtWidgets.QWidget(bottom_widget) - - # Convert button widget (with layout to handle stretch) - convert_widget = QtWidgets.QWidget(creator_widget) - convert_label = QtWidgets.QLabel(creator_widget) - # Set the label text with 'setText' to apply html - convert_label.setText( - ( - "Found old publishable products" - " incompatible with new publisher." - "

Press the update products button" - " to automatically update them" - " to be able to publish again." - ) - ) - convert_label.setWordWrap(True) - convert_label.setAlignment(QtCore.Qt.AlignCenter) - - convert_btn = QtWidgets.QPushButton( - "Update products", convert_widget - ) - convert_separator = QtWidgets.QFrame(convert_widget) - convert_separator.setObjectName("Separator") - convert_separator.setMinimumHeight(1) - convert_separator.setMaximumHeight(1) - - convert_layout = QtWidgets.QGridLayout(convert_widget) - convert_layout.setContentsMargins(5, 0, 5, 0) - convert_layout.setVerticalSpacing(10) - convert_layout.addWidget(convert_label, 0, 0, 1, 3) - convert_layout.addWidget(convert_btn, 1, 1) - convert_layout.addWidget(convert_separator, 2, 0, 1, 3) - convert_layout.setColumnStretch(0, 1) - convert_layout.setColumnStretch(1, 0) - convert_layout.setColumnStretch(2, 1) - - # Creator attributes widget - creator_attrs_widget = CreatorAttrsWidget( - controller, creator_widget - ) - creator_layout = QtWidgets.QVBoxLayout(creator_widget) - creator_layout.setContentsMargins(0, 0, 0, 0) - creator_layout.addWidget(convert_widget, 0) - creator_layout.addWidget(creator_attrs_widget, 1) - - publish_attrs_widget = PublishPluginAttrsWidget( - controller, bottom_widget - ) - - bottom_separator = QtWidgets.QWidget(bottom_widget) - bottom_separator.setObjectName("Separator") - bottom_separator.setMinimumWidth(1) - - bottom_layout = QtWidgets.QHBoxLayout(bottom_widget) - bottom_layout.setContentsMargins(0, 0, 0, 0) - bottom_layout.addWidget(creator_widget, 1) - bottom_layout.addWidget(bottom_separator, 0) - bottom_layout.addWidget(publish_attrs_widget, 1) - - top_bottom = QtWidgets.QWidget(self) - top_bottom.setObjectName("Separator") - top_bottom.setMinimumHeight(1) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(top_widget, 0) - layout.addWidget(top_bottom, 0) - layout.addWidget(bottom_widget, 1) - - self._convertor_identifiers = None - self._current_instances = None - self._context_selected = False - self._all_instances_valid = True - - global_attrs_widget.instance_context_changed.connect( - self._on_instance_context_changed - ) - convert_btn.clicked.connect(self._on_convert_click) - thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) - thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) - - controller.register_event_callback( - "instance.thumbnail.changed", self._on_thumbnail_changed - ) - - self._controller: AbstractPublisherFrontend = controller - - self._convert_widget = convert_widget - - self.global_attrs_widget = global_attrs_widget - - self.creator_attrs_widget = creator_attrs_widget - self.publish_attrs_widget = publish_attrs_widget - self._thumbnail_widget = thumbnail_widget - - self.top_bottom = top_bottom - self.bottom_separator = bottom_separator - - def _on_instance_context_changed(self): - instance_ids = { - instance.id - for instance in self._current_instances - } - context_info_by_id = self._controller.get_instances_context_info( - instance_ids - ) - all_valid = True - for instance_id, context_info in context_info_by_id.items(): - if not context_info.is_valid: - all_valid = False - break - - self._all_instances_valid = all_valid - self.creator_attrs_widget.set_instances_valid(all_valid) - self.publish_attrs_widget.set_instances_valid(all_valid) - - self.instance_context_changed.emit() - - def _on_convert_click(self): - self.convert_requested.emit() - - def set_current_instances( - self, instances, context_selected, convertor_identifiers - ): - """Change currently selected items. - - Args: - instances (List[InstanceItem]): List of currently selected - instances. - context_selected (bool): Is context selected. - convertor_identifiers (List[str]): Identifiers of convert items. - """ - - instance_ids = { - instance.id - for instance in instances - } - context_info_by_id = self._controller.get_instances_context_info( - instance_ids - ) - - all_valid = True - for context_info in context_info_by_id.values(): - if not context_info.is_valid: - all_valid = False - break - - s_convertor_identifiers = set(convertor_identifiers) - self._convertor_identifiers = s_convertor_identifiers - self._current_instances = instances - self._context_selected = context_selected - self._all_instances_valid = all_valid - - self._convert_widget.setVisible(len(s_convertor_identifiers) > 0) - self.global_attrs_widget.set_current_instances(instances) - self.creator_attrs_widget.set_current_instances(instance_ids) - self.publish_attrs_widget.set_current_instances( - instance_ids, context_selected - ) - self.creator_attrs_widget.set_instances_valid(all_valid) - self.publish_attrs_widget.set_instances_valid(all_valid) - - self._update_thumbnails() - - def _on_thumbnail_create(self, path): - instance_ids = [ - instance.id - for instance in self._current_instances - ] - if self._context_selected: - instance_ids.append(None) - - if not instance_ids: - return - - mapping = {} - if len(instance_ids) == 1: - mapping[instance_ids[0]] = path - - else: - for instance_id in instance_ids: - root = os.path.dirname(path) - ext = os.path.splitext(path)[-1] - dst_path = os.path.join(root, str(uuid.uuid4()) + ext) - shutil.copy(path, dst_path) - mapping[instance_id] = dst_path - - self._controller.set_thumbnail_paths_for_instances(mapping) - - def _on_thumbnail_clear(self): - instance_ids = [ - instance.id - for instance in self._current_instances - ] - if self._context_selected: - instance_ids.append(None) - - if not instance_ids: - return - - mapping = { - instance_id: None - for instance_id in instance_ids - } - self._controller.set_thumbnail_paths_for_instances(mapping) - - def _on_thumbnail_changed(self, event): - self._update_thumbnails() - - def _update_thumbnails(self): - instance_ids = [ - instance.id - for instance in self._current_instances - ] - if self._context_selected: - instance_ids.append(None) - - if not instance_ids: - self._thumbnail_widget.setVisible(False) - self._thumbnail_widget.set_current_thumbnails(None) - return - - mapping = self._controller.get_thumbnail_paths_for_instances( - instance_ids - ) - thumbnail_paths = [] - for instance_id in instance_ids: - path = mapping[instance_id] - if path: - thumbnail_paths.append(path) - - self._thumbnail_widget.setVisible(True) - self._thumbnail_widget.set_current_thumbnails(thumbnail_paths) - - class CreateNextPageOverlay(QtWidgets.QWidget): clicked = QtCore.Signal() From ec5f4abcd79456f5da3eade3b67df5d17061660c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:49:56 +0200 Subject: [PATCH 055/546] removemend happens in bulk --- client/ayon_core/pipeline/create/context.py | 41 +++++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a6c42ef70a..29cb9d3d4b 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -235,6 +235,7 @@ class CreateContext: self._bulk_info = { # Collect instances "collect": BulkInfo(), + "remove": BulkInfo(), # Change values of instances or create context "change": BulkInfo(), # Create attribute definitions changed @@ -1070,6 +1071,11 @@ class CreateContext: plugin.__name__, attr_defs ) + @contextmanager + def bulk_remove_instances(self, sender=None): + with self._bulk_context("remove", sender) as bulk_info: + yield bulk_info + @contextmanager def bulk_value_changes(self, sender=None): with self._bulk_context("change", sender) as bulk_info: @@ -1165,6 +1171,8 @@ class CreateContext: data = bulk_info.pop_data() if key == "collect": self._bulk_instances_collection(data, sender) + elif key == "remove": + self._bulk_remove_instances_finished(data, sender) elif key == "change": self._bulk_values_change(data, sender) elif key == "create_attrs_change": @@ -1187,6 +1195,18 @@ class CreateContext: sender, ) + def _bulk_remove_instances_finished(self, instances_to_remove, sender): + if not instances_to_remove: + return + + self._emit_event( + INSTANCE_REMOVED_TOPIC, + { + "instances": instances_to_remove, + }, + sender, + ) + def _bulk_values_change( self, changes: Tuple[Union[str, None], Dict[str, Any]], @@ -1736,22 +1756,11 @@ class CreateContext: return self._event_hub.emit(topic, data, sender) def _remove_instances(self, instances, sender=None): - removed_instances = [] - for instance in instances: - obj = self._instances_by_id.pop(instance.id, None) - if obj is not None: - removed_instances.append(instance) - - if not removed_instances: - return - - self._emit_event( - INSTANCE_REMOVED_TOPIC, - { - "instances": removed_instances, - }, - sender, - ) + with self.bulk_remove_instances(sender) as bulk_info: + for instance in instances: + obj = self._instances_by_id.pop(instance.id, None) + if obj is not None: + bulk_info.append(obj) def _create_with_unified_error( self, identifier, creator, *args, **kwargs From cadc2e6551bdf929b3001725584a319f1cf3da51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:07:21 +0200 Subject: [PATCH 056/546] change collect to add --- client/ayon_core/pipeline/create/context.py | 31 ++++++++++++------- .../tools/publisher/models/create.py | 2 +- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 29cb9d3d4b..6872632a57 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -234,7 +234,7 @@ class CreateContext: # after leaving of last context manager scope self._bulk_info = { # Collect instances - "collect": BulkInfo(), + "add": BulkInfo(), "remove": BulkInfo(), # Change values of instances or create context "change": BulkInfo(), @@ -527,7 +527,7 @@ class CreateContext: self.reset_plugins(discover_publish_plugins) self.reset_context_data() - with self.bulk_instances_collection(): + with self.bulk_add_instances(): self.reset_instances() self.find_convertor_items() self.execute_autocreators() @@ -841,9 +841,9 @@ class CreateContext: self._instances_by_id[instance.id] = instance - # Add instance to be validated inside 'bulk_instances_collection' + # Add instance to be validated inside 'bulk_add_instances' # context manager if is inside bulk - with self.bulk_instances_collection() as bulk_info: + with self.bulk_add_instances() as bulk_info: bulk_info.append(instance) def _get_creator_in_create(self, identifier): @@ -970,7 +970,7 @@ class CreateContext: active = bool(active) instance_data["active"] = active - with self.bulk_instances_collection(): + with self.bulk_add_instances(): return creator.create( product_name, instance_data, @@ -1023,8 +1023,8 @@ class CreateContext: self.convertor_items_by_id.pop(convertor_identifier, None) @contextmanager - def bulk_instances_collection(self, sender=None): - with self._bulk_context("collect", sender) as bulk_info: + def bulk_add_instances(self, sender=None): + with self._bulk_context("add", sender) as bulk_info: yield bulk_info # Set publish attributes before bulk context is exited @@ -1071,6 +1071,13 @@ class CreateContext: plugin.__name__, attr_defs ) + @contextmanager + def bulk_instances_collection(self, sender=None): + """DEPRECATED use 'bulk_add_instances' instead.""" + # TODO add warning + with self.bulk_add_instances(sender) as bulk_info: + yield bulk_info + @contextmanager def bulk_remove_instances(self, sender=None): with self._bulk_context("remove", sender) as bulk_info: @@ -1101,7 +1108,7 @@ class CreateContext: return False # Instance in 'collect' bulk will be ignored - for instance in self._bulk_info["collect"].get_data(): + for instance in self._bulk_info["add"].get_data(): if instance.id == instance_id: return False return True @@ -1169,8 +1176,8 @@ class CreateContext: bulk_info = self._bulk_info[key] sender = bulk_info.get_sender() data = bulk_info.pop_data() - if key == "collect": - self._bulk_instances_collection(data, sender) + if key == "add": + self._bulk_add_instances_finished(data, sender) elif key == "remove": self._bulk_remove_instances_finished(data, sender) elif key == "change": @@ -1180,7 +1187,7 @@ class CreateContext: elif key == "publish_attrs_change": self._bulk_publish_attrs_change(data, sender) - def _bulk_instances_collection(self, instances_to_validate, sender): + def _bulk_add_instances_finished(self, instances_to_validate, sender): if not instances_to_validate: return @@ -1781,7 +1788,7 @@ class CreateContext: label = getattr(creator, "label", label) # Run create - with self.bulk_instances_collection(): + with self.bulk_add_instances(): result = creator.create(*args, **kwargs) success = True diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index f7e8aaa503..e1eac3ae5b 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -808,7 +808,7 @@ class CreateModel: """Reset create instances.""" self._create_context.reset_context_data() - with self._create_context.bulk_instances_collection(): + with self._create_context.bulk_add_instances(): try: self._create_context.reset_instances() except CreatorsOperationFailed as exc: From adcdab0b0340e62304c4b8154a783957c6738673 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:07:35 +0200 Subject: [PATCH 057/546] run create with in bulk mode --- client/ayon_core/tools/publisher/models/create.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index e1eac3ae5b..8f18e5f523 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -526,9 +526,10 @@ class CreateModel: success = True try: - self._create_context.create_with_unified_error( - creator_identifier, product_name, instance_data, options - ) + with self._create_context.bulk_add_instances(): + self._create_context.create_with_unified_error( + creator_identifier, product_name, instance_data, options + ) except CreatorsOperationFailed as exc: success = False @@ -661,6 +662,7 @@ class CreateModel: value = None if attr_def.is_value_def: value = instance.creator_attributes[attr_def.key] + if found_idx is None: idx = len(output) output.append((attr_def, [instance_id], [value])) From 903c18c3e1e4dd999c9d15b35153a91fc56e4694 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:07:47 +0200 Subject: [PATCH 058/546] formatting fix --- client/ayon_core/pipeline/create/context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6872632a57..693a11db18 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1150,8 +1150,7 @@ class CreateContext: bulk_info.increase() if key not in self._bulk_order: - self._bulk_order.append(key -) + self._bulk_order.append(key) try: yield bulk_info finally: From cdc8e7c7a1449ad39a533db15086360b39731aa3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:08:00 +0200 Subject: [PATCH 059/546] added finished suffix to finishing methods --- client/ayon_core/pipeline/create/context.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 693a11db18..66c27f7159 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1180,11 +1180,11 @@ class CreateContext: elif key == "remove": self._bulk_remove_instances_finished(data, sender) elif key == "change": - self._bulk_values_change(data, sender) + self._bulk_values_change_finished(data, sender) elif key == "create_attrs_change": - self._bulk_create_attrs_change(data, sender) + self._bulk_create_attrs_change_finished(data, sender) elif key == "publish_attrs_change": - self._bulk_publish_attrs_change(data, sender) + self._bulk_publish_attrs_change_finished(data, sender) def _bulk_add_instances_finished(self, instances_to_validate, sender): if not instances_to_validate: @@ -1213,7 +1213,7 @@ class CreateContext: sender, ) - def _bulk_values_change( + def _bulk_values_change_finished( self, changes: Tuple[Union[str, None], Dict[str, Any]], sender: Optional[str], @@ -1263,7 +1263,7 @@ class CreateContext: sender ) - def _bulk_create_attrs_change( + def _bulk_create_attrs_change_finished( self, instance_ids: List[str], sender: Optional[str] ): if not instance_ids: @@ -1281,7 +1281,7 @@ class CreateContext: sender, ) - def _bulk_publish_attrs_change( + def _bulk_publish_attrs_change_finished( self, attr_info: Tuple[str, Union[str, None]], sender: Optional[str], From 7338cffba3a05b4f7af64be0c880325f72120891 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:08:17 +0200 Subject: [PATCH 060/546] fix bulk order processing --- client/ayon_core/pipeline/create/context.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 66c27f7159..89f69bc452 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1162,14 +1162,15 @@ class CreateContext: if self._bulk_order[0] != key: return - self._bulk_finish(key) self._bulk_order.pop(0) + self._bulk_finish(key) - for key in tuple(self._bulk_order): + while self._bulk_order: + key = self._bulk_order[0] if not self._bulk_info[key]: - return - self._bulk_finish(key) + break self._bulk_order.pop(0) + self._bulk_finish(key) def _bulk_finish(self, key): bulk_info = self._bulk_info[key] From c6043316a70c69185dce3c2d20556796a58daa6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:08:24 +0200 Subject: [PATCH 061/546] fix typo --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 89f69bc452..d86dbea9fa 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1004,7 +1004,7 @@ class CreateContext: def creator_removed_instance(self, instance: "CreatedInstance"): """When creator removes instance context should be acknowledged. - If creator removes instance conext should know about it to avoid + If creator removes instance context should know about it to avoid possible issues in the session. Args: From 092eacf534e6de6dd01637460807db89aedc0858 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:08:38 +0200 Subject: [PATCH 062/546] unpack tuple to variables --- client/ayon_core/tools/publisher/models/create.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 8f18e5f523..a2530cacc6 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -668,9 +668,9 @@ class CreateModel: output.append((attr_def, [instance_id], [value])) _attr_defs[idx] = attr_def else: - item = output[found_idx] - item[1].append(instance_id) - item[2].append(value) + _, ids, values = output[found_idx] + ids.append(instance_id) + values.append(value) return output def set_instances_publish_attr_values( From dcbdfa09bddd98d2f7a3c2b59972a535cd1f8963 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:09:11 +0200 Subject: [PATCH 063/546] move private methods lower --- .../tools/publisher/widgets/product_info.py | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_info.py b/client/ayon_core/tools/publisher/widgets/product_info.py index c1d2037bd3..abd1408747 100644 --- a/client/ayon_core/tools/publisher/widgets/product_info.py +++ b/client/ayon_core/tools/publisher/widgets/product_info.py @@ -147,29 +147,6 @@ class ProductInfoWidget(QtWidgets.QWidget): self.top_bottom = top_bottom self.bottom_separator = bottom_separator - def _on_instance_context_changed(self): - instance_ids = { - instance.id - for instance in self._current_instances - } - context_info_by_id = self._controller.get_instances_context_info( - instance_ids - ) - all_valid = True - for instance_id, context_info in context_info_by_id.items(): - if not context_info.is_valid: - all_valid = False - break - - self._all_instances_valid = all_valid - self.creator_attrs_widget.set_instances_valid(all_valid) - self.publish_attrs_widget.set_instances_valid(all_valid) - - self.instance_context_changed.emit() - - def _on_convert_click(self): - self.convert_requested.emit() - def set_current_instances( self, instances, context_selected, convertor_identifiers ): @@ -213,6 +190,29 @@ class ProductInfoWidget(QtWidgets.QWidget): self._update_thumbnails() + def _on_instance_context_changed(self): + instance_ids = { + instance.id + for instance in self._current_instances + } + context_info_by_id = self._controller.get_instances_context_info( + instance_ids + ) + all_valid = True + for instance_id, context_info in context_info_by_id.items(): + if not context_info.is_valid: + all_valid = False + break + + self._all_instances_valid = all_valid + self.creator_attrs_widget.set_instances_valid(all_valid) + self.publish_attrs_widget.set_instances_valid(all_valid) + + self.instance_context_changed.emit() + + def _on_convert_click(self): + self.convert_requested.emit() + def _on_thumbnail_create(self, path): instance_ids = [ instance.id From b68287f69defaff6848d2d4d6ab2c80e348eb286 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:35:18 +0200 Subject: [PATCH 064/546] refresh instances in separate method --- .../tools/publisher/widgets/product_info.py | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_info.py b/client/ayon_core/tools/publisher/widgets/product_info.py index abd1408747..9a7700d73d 100644 --- a/client/ayon_core/tools/publisher/widgets/product_info.py +++ b/client/ayon_core/tools/publisher/widgets/product_info.py @@ -119,7 +119,7 @@ class ProductInfoWidget(QtWidgets.QWidget): layout.addWidget(bottom_widget, 1) self._convertor_identifiers = None - self._current_instances = None + self._current_instances = [] self._context_selected = False self._all_instances_valid = True @@ -131,7 +131,8 @@ class ProductInfoWidget(QtWidgets.QWidget): thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) controller.register_event_callback( - "instance.thumbnail.changed", self._on_thumbnail_changed + "instance.thumbnail.changed", + self._on_thumbnail_changed ) self._controller: AbstractPublisherFrontend = controller @@ -157,11 +158,18 @@ class ProductInfoWidget(QtWidgets.QWidget): instances. context_selected (bool): Is context selected. convertor_identifiers (List[str]): Identifiers of convert items. - """ + """ + s_convertor_identifiers = set(convertor_identifiers) + self._current_instances = instances + self._context_selected = context_selected + self._convertor_identifiers = s_convertor_identifiers + self._refresh_instances() + + def _refresh_instances(self): instance_ids = { instance.id - for instance in instances + for instance in self._current_instances } context_info_by_id = self._controller.get_instances_context_info( instance_ids @@ -173,17 +181,15 @@ class ProductInfoWidget(QtWidgets.QWidget): all_valid = False break - s_convertor_identifiers = set(convertor_identifiers) - self._convertor_identifiers = s_convertor_identifiers - self._current_instances = instances - self._context_selected = context_selected self._all_instances_valid = all_valid - self._convert_widget.setVisible(len(s_convertor_identifiers) > 0) - self.global_attrs_widget.set_current_instances(instances) + self._convert_widget.setVisible(len(self._convertor_identifiers) > 0) + self.global_attrs_widget.set_current_instances( + self._current_instances + ) self.creator_attrs_widget.set_current_instances(instance_ids) self.publish_attrs_widget.set_current_instances( - instance_ids, context_selected + instance_ids, self._context_selected ) self.creator_attrs_widget.set_instances_valid(all_valid) self.publish_attrs_widget.set_instances_valid(all_valid) From dd276c230a636c72e6ba1261eba01c0848363bd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:47:47 +0200 Subject: [PATCH 065/546] fix typehings and costrings --- client/ayon_core/tools/publisher/abstract.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 196c1c938e..9a81fa0ac0 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -77,7 +77,7 @@ class AbstractPublisherCommon(ABC): in future e.g. different message timeout or type (color). Args: - message (str): Message that will be showed. + message (str): Message that will be shown. message_type (Optional[str]): Message type. """ @@ -202,7 +202,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): def is_host_valid(self) -> bool: """Host is valid for creation part. - Host must have implemented certain functionality to be able create + Host must have implemented certain functionality to be able to create in Publisher tool. Returns: @@ -333,7 +333,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod def get_creator_attribute_definitions( - self, instance_ids: List[str] + self, instance_ids: Iterable[str] ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: pass @@ -346,7 +346,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod def get_publish_attribute_definitions( self, - instance_ids: List[str], + instance_ids: Iterable[str], include_context: bool ) -> List[Tuple[ str, @@ -398,7 +398,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): """Trigger creation by creator identifier. - Should also trigger refresh of instanes. + Should also trigger refresh of instances. Args: creator_identifier (str): Identifier of Creator plugin. @@ -461,8 +461,8 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): """Trigger pyblish action on a plugin. Args: - plugin_id (str): Id of publish plugin. - action_id (str): Id of publish action. + plugin_id (str): Publish plugin id. + action_id (str): Publish action id. """ pass @@ -601,7 +601,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod def get_thumbnail_temp_dir_path(self) -> str: - """Return path to directory where thumbnails can be temporary stored. + """Path to directory where thumbnails can be temporarily stored. Returns: str: Path to a directory. From dec2fb740de0a31041bf2628f213285de014194d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:51:46 +0200 Subject: [PATCH 066/546] refresh create and publish attributes on change --- .../publisher/widgets/product_attributes.py | 71 ++++++++++++++++--- 1 file changed, 61 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index a696907a72..6478748f7a 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -41,6 +41,10 @@ class CreatorAttrsWidget(QtWidgets.QWidget): "create.context.create.attrs.changed", self._on_instance_attr_defs_change ) + controller.register_event_callback( + "create.context.value.changed", + self._on_instance_value_change + ) self._main_layout = main_layout @@ -67,6 +71,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): """Set current instances for which are attribute definitions shown.""" self._current_instance_ids = set(instance_ids) + self._refresh_content() + + def _refresh_content(self): prev_content_widget = self._scroll_area.widget() if prev_content_widget: self._scroll_area.takeWidget() @@ -78,7 +85,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_attr_def = {} result = self._controller.get_creator_attribute_definitions( - instance_ids + self._current_instance_ids ) content_widget = QtWidgets.QWidget(self._scroll_area) @@ -141,14 +148,21 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._content_widget = content_widget def _on_instance_attr_defs_change(self, event): - update = False for instance_id in event.data["instance_ids"]: if instance_id in self._current_instance_ids: - update = True + self._refresh_content() break - if update: - self.set_current_instances(self._current_instance_ids) + def _on_instance_value_change(self, event): + # TODO try to find more optimized way to update values instead of + # force refresh of all of them. + for instance_id, changes in event["instance_changes"].items(): + if ( + instance_id in self._current_instance_ids + and "creator_attributes" not in changes + ): + self._refresh_content() + break def _input_value_changed(self, value, attr_id): instance_ids = self._attr_def_id_to_instances.get(attr_id) @@ -161,9 +175,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): class PublishPluginAttrsWidget(QtWidgets.QWidget): - """Widget showing publsish plugin attributes for selected instances. + """Widget showing publish plugin attributes for selected instances. - Attributes are defined on publish plugins. Publihs plugin may define + Attributes are defined on publish plugins. Publish plugin may define attribute definitions but must inherit `AYONPyblishPluginMixin` (~/ayon_core/pipeline/publish). At the moment requires to implement `get_attribute_defs` and `convert_attribute_values` class methods. @@ -192,6 +206,18 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): main_layout.setSpacing(0) main_layout.addWidget(scroll_area, 1) + controller.register_event_callback( + "create.context.publish.attrs.changed", + self._on_instance_attr_defs_change + ) + controller.register_event_callback( + "create.context.value.changed", + self._on_instance_value_change + ) + + self._instance_ids = set() + self._context_selected = False + self._main_layout = main_layout self._controller: AbstractPublisherFrontend = controller @@ -215,6 +241,11 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): def set_current_instances(self, instance_ids, context_selected): """Set current instances for which are attribute definitions shown.""" + self._instance_ids = set(instance_ids) + self._context_selected = context_selected + self._refresh_content() + + def _refresh_content(self): prev_content_widget = self._scroll_area.widget() if prev_content_widget: self._scroll_area.takeWidget() @@ -228,7 +259,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_plugin_name = {} result = self._controller.get_publish_attribute_definitions( - instance_ids, context_selected + self._instance_ids, self._context_selected ) content_widget = QtWidgets.QWidget(self._scroll_area) @@ -253,8 +284,8 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): ) hidden_widget = attr_def.hidden # Hide unknown values of publish plugins - # - The keys in most of cases does not represent what would - # label represent + # - The keys in most of the cases does not represent what + # would label represent if isinstance(attr_def, UnknownDef): widget.setVisible(False) hidden_widget = True @@ -323,3 +354,23 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._controller.set_instances_publish_attr_values( instance_ids, plugin_name, attr_def.key, value ) + + def _on_instance_attr_defs_change(self, event): + for instance_id in event.data: + if ( + instance_id is None and self._context_selected + or instance_id in self._instance_ids + ): + self._refresh_content() + break + + def _on_instance_value_change(self, event): + # TODO try to find more optimized way to update values instead of + # force refresh of all of them. + for instance_id, changes in event["instance_changes"].items(): + if ( + instance_id in self._current_instance_ids + and "publish_attributes" not in changes + ): + self._refresh_content() + break From c4f5475014aee6b905c90e5e07026616cc98e019 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:51:52 +0200 Subject: [PATCH 067/546] added missing import --- client/ayon_core/tools/publisher/models/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/__init__.py b/client/ayon_core/tools/publisher/models/__init__.py index 07f061deaa..26eeb3cdbb 100644 --- a/client/ayon_core/tools/publisher/models/__init__.py +++ b/client/ayon_core/tools/publisher/models/__init__.py @@ -1,10 +1,11 @@ -from .create import CreateModel, CreatorItem +from .create import CreateModel, CreatorItem, InstanceItem from .publish import PublishModel, PublishErrorInfo __all__ = ( "CreateModel", "CreatorItem", + "InstanceItem", "PublishModel", "PublishErrorInfo", From 279b2f886b23841247836287740ddf6d71daa62e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:22:36 +0200 Subject: [PATCH 068/546] there is option to update precreate attributes --- client/ayon_core/pipeline/create/context.py | 452 +++++++++++------- .../pipeline/create/creator_plugins.py | 10 + 2 files changed, 278 insertions(+), 184 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index d86dbea9fa..9cde82db10 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -72,6 +72,7 @@ _NOT_SET = object() INSTANCE_ADDED_TOPIC = "instances.added" INSTANCE_REMOVED_TOPIC = "instances.removed" VALUE_CHANGED_TOPIC = "values.changed" +PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC = "pre.create.attr.defs.changed" CREATE_ATTR_DEFS_CHANGED_TOPIC = "create.attr.defs.changed" PUBLISH_ATTR_DEFS_CHANGED_TOPIC = "publish.attr.defs.changed" @@ -233,11 +234,14 @@ class CreateContext: # using context manager which will trigger validation # after leaving of last context manager scope self._bulk_info = { - # Collect instances + # Added instances "add": BulkInfo(), + # Removed instances "remove": BulkInfo(), # Change values of instances or create context "change": BulkInfo(), + # Pre create attribute definitions changed + "pre_create_attrs_change": BulkInfo(), # Create attribute definitions changed "create_attrs_change": BulkInfo(), # Publish attribute definitions changed @@ -538,7 +542,7 @@ class CreateContext: """Cleanup thumbnail paths. Remove all thumbnail filepaths that are empty or lead to files which - does not exists or of instances that are not available anymore. + does not exist or of instances that are not available anymore. """ invalid = set() @@ -1088,6 +1092,11 @@ class CreateContext: with self._bulk_context("change", sender) as bulk_info: yield bulk_info + @contextmanager + def bulk_pre_create_attr_defs_change(self, sender=None): + with self._bulk_context("pre_create_attrs_change", sender) as bulk_info: + yield bulk_info + @contextmanager def bulk_create_attr_defs_change(self, sender=None): with self._bulk_context("create_attrs_change", sender) as bulk_info: @@ -1099,41 +1108,78 @@ class CreateContext: yield bulk_info # --- instance change callbacks --- - def _is_instance_events_ready(self, instance_id): - # Context is ready - if instance_id is None: - return True - # Instance is not in yet in context - if instance_id not in self._instances_by_id: - return False + def create_plugin_pre_create_attr_defs_changed(self, identifier: str): + """Create plugin pre-create attributes changed. - # Instance in 'collect' bulk will be ignored - for instance in self._bulk_info["add"].get_data(): - if instance.id == instance_id: - return False - return True + Triggered by 'Creator'. - def instance_create_attr_defs_changed(self, instance_id): + Args: + identifier (str): Create plugin identifier. + + """ + with self.bulk_pre_create_attr_defs_change() as bulk_item: + bulk_item.append(identifier) + + def instance_create_attr_defs_changed(self, instance_id: str): + """Instance attribute definitions changed. + + Triggered by instance 'CreatorAttributeValues' on instance. + + Args: + instance_id (str): Instance id. + + """ if self._is_instance_events_ready(instance_id): with self.bulk_create_attr_defs_change() as bulk_item: bulk_item.append(instance_id) def instance_publish_attr_defs_changed( - self, instance_id, plugin_name + self, instance_id: Optional[str], plugin_name: str ): + """Instance attribute definitions changed. + + Triggered by instance 'PublishAttributeValues' on instance. + + Args: + instance_id (Optional[str]): Instance id or None for context. + plugin_name (str): Plugin name which attribute definitions were + changed. + + """ if self._is_instance_events_ready(instance_id): with self.bulk_publish_attr_defs_change() as bulk_item: bulk_item.append((instance_id, plugin_name)) def instance_values_changed( - self, instance_id, new_values + self, instance_id: Optional[str], new_values: Dict[str, Any] ): + """Instance value changed. + + Triggered by `CreatedInstance, 'CreatorAttributeValues' + or 'PublishAttributeValues' on instance. + + Args: + instance_id (Optional[str]): Instance id or None for context. + new_values (Dict[str, Any]): Changed values. + + """ if self._is_instance_events_ready(instance_id): with self.bulk_value_changes() as bulk_item: bulk_item.append((instance_id, new_values)) # --- context change callbacks --- - def publish_attribute_value_changed(self, plugin_name, value): + def publish_attribute_value_changed( + self, plugin_name: str, value: Dict[str, Any] + ): + """Context publish attribute values changed. + + Triggered by instance 'PublishAttributeValues' on context. + + Args: + plugin_name (str): Plugin name which changed value. + value (Dict[str, Any]): Changed values. + + """ self.instance_values_changed( None, { @@ -1143,172 +1189,6 @@ class CreateContext: }, ) - @contextmanager - def _bulk_context(self, key, sender): - bulk_info = self._bulk_info[key] - bulk_info.set_sender(sender) - - bulk_info.increase() - if key not in self._bulk_order: - self._bulk_order.append(key) - try: - yield bulk_info - finally: - bulk_info.decrease() - if bulk_info: - self._bulk_finished(key) - - def _bulk_finished(self, key): - if self._bulk_order[0] != key: - return - - self._bulk_order.pop(0) - self._bulk_finish(key) - - while self._bulk_order: - key = self._bulk_order[0] - if not self._bulk_info[key]: - break - self._bulk_order.pop(0) - self._bulk_finish(key) - - def _bulk_finish(self, key): - bulk_info = self._bulk_info[key] - sender = bulk_info.get_sender() - data = bulk_info.pop_data() - if key == "add": - self._bulk_add_instances_finished(data, sender) - elif key == "remove": - self._bulk_remove_instances_finished(data, sender) - elif key == "change": - self._bulk_values_change_finished(data, sender) - elif key == "create_attrs_change": - self._bulk_create_attrs_change_finished(data, sender) - elif key == "publish_attrs_change": - self._bulk_publish_attrs_change_finished(data, sender) - - def _bulk_add_instances_finished(self, instances_to_validate, sender): - if not instances_to_validate: - return - - # Cache folder and task entities for all instances at once - self.get_instances_context_info(instances_to_validate) - - self._emit_event( - INSTANCE_ADDED_TOPIC, - { - "instances": instances_to_validate, - }, - sender, - ) - - def _bulk_remove_instances_finished(self, instances_to_remove, sender): - if not instances_to_remove: - return - - self._emit_event( - INSTANCE_REMOVED_TOPIC, - { - "instances": instances_to_remove, - }, - sender, - ) - - def _bulk_values_change_finished( - self, - changes: Tuple[Union[str, None], Dict[str, Any]], - sender: Optional[str], - ): - if not changes: - return - item_data_by_id = {} - for item_id, item_changes in changes: - item_values = item_data_by_id.setdefault(item_id, {}) - if "creator_attributes" in item_changes: - current_value = item_values.setdefault( - "creator_attributes", {} - ) - current_value.update( - item_changes.pop("creator_attributes") - ) - - if "publish_attributes" in item_changes: - current_publish = item_values.setdefault( - "publish_attributes", {} - ) - for plugin_name, plugin_value in item_changes.pop( - "publish_attributes" - ).items(): - plugin_changes = current_publish.setdefault( - plugin_name, {} - ) - plugin_changes.update(plugin_value) - - item_values.update(item_changes) - - event_changes = [] - for item_id, item_changes in item_data_by_id.items(): - instance = self.get_instance_by_id(item_id) - event_changes.append({ - "instance": instance, - "changes": item_changes, - }) - - event_data = { - "changes": event_changes, - } - - self._emit_event( - VALUE_CHANGED_TOPIC, - event_data, - sender - ) - - def _bulk_create_attrs_change_finished( - self, instance_ids: List[str], sender: Optional[str] - ): - if not instance_ids: - return - - instances = [ - self.get_instance_by_id(instance_id) - for instance_id in set(instance_ids) - ] - self._emit_event( - CREATE_ATTR_DEFS_CHANGED_TOPIC, - { - "instances": instances, - }, - sender, - ) - - def _bulk_publish_attrs_change_finished( - self, - attr_info: Tuple[str, Union[str, None]], - sender: Optional[str], - ): - if not attr_info: - return - - instance_changes = {} - for instance_id, plugin_name in attr_info: - instance_data = instance_changes.setdefault( - instance_id, - { - "instance": None, - "plugin_names": set(), - } - ) - instance = self.get_instance_by_id(instance_id) - instance_data["instance"] = instance - instance_data["plugin_names"].add(plugin_name) - - self._emit_event( - PUBLISH_ATTR_DEFS_CHANGED_TOPIC, - {"instance_changes": instance_changes}, - sender, - ) - def reset_instances(self): """Reload instances""" self._instances_by_id = collections.OrderedDict() @@ -1809,3 +1689,207 @@ class CreateContext: identifier, label, exc_info, add_traceback ) return result, fail_info + + def _is_instance_events_ready(self, instance_id: Optional[str]) -> bool: + # Context is ready + if instance_id is None: + return True + # Instance is not in yet in context + if instance_id not in self._instances_by_id: + return False + + # Instance in 'collect' bulk will be ignored + for instance in self._bulk_info["add"].get_data(): + if instance.id == instance_id: + return False + return True + + @contextmanager + def _bulk_context(self, key: str, sender: Optional[str]): + bulk_info = self._bulk_info[key] + bulk_info.set_sender(sender) + + bulk_info.increase() + if key not in self._bulk_order: + self._bulk_order.append(key) + try: + yield bulk_info + finally: + bulk_info.decrease() + if bulk_info: + self._bulk_finished(key) + + def _bulk_finished(self, key: str): + if self._bulk_order[0] != key: + return + + self._bulk_order.pop(0) + self._bulk_finish(key) + + while self._bulk_order: + key = self._bulk_order[0] + if not self._bulk_info[key]: + break + self._bulk_order.pop(0) + self._bulk_finish(key) + + def _bulk_finish(self, key: str): + bulk_info = self._bulk_info[key] + sender = bulk_info.get_sender() + data = bulk_info.pop_data() + if key == "add": + self._bulk_add_instances_finished(data, sender) + elif key == "remove": + self._bulk_remove_instances_finished(data, sender) + elif key == "change": + self._bulk_values_change_finished(data, sender) + elif key == "pre_create_attrs_change": + self._bulk_pre_create_attrs_change_finished(data, sender) + elif key == "create_attrs_change": + self._bulk_create_attrs_change_finished(data, sender) + elif key == "publish_attrs_change": + self._bulk_publish_attrs_change_finished(data, sender) + + def _bulk_add_instances_finished( + self, + instances_to_validate: List["CreatedInstance"], + sender: Optional[str] + ): + if not instances_to_validate: + return + + # Cache folder and task entities for all instances at once + self.get_instances_context_info(instances_to_validate) + + self._emit_event( + INSTANCE_ADDED_TOPIC, + { + "instances": instances_to_validate, + }, + sender, + ) + + def _bulk_remove_instances_finished( + self, + instances_to_remove: List["CreatedInstance"], + sender: Optional[str] + ): + if not instances_to_remove: + return + + self._emit_event( + INSTANCE_REMOVED_TOPIC, + { + "instances": instances_to_remove, + }, + sender, + ) + + def _bulk_values_change_finished( + self, + changes: Tuple[Union[str, None], Dict[str, Any]], + sender: Optional[str], + ): + if not changes: + return + item_data_by_id = {} + for item_id, item_changes in changes: + item_values = item_data_by_id.setdefault(item_id, {}) + if "creator_attributes" in item_changes: + current_value = item_values.setdefault( + "creator_attributes", {} + ) + current_value.update( + item_changes.pop("creator_attributes") + ) + + if "publish_attributes" in item_changes: + current_publish = item_values.setdefault( + "publish_attributes", {} + ) + for plugin_name, plugin_value in item_changes.pop( + "publish_attributes" + ).items(): + plugin_changes = current_publish.setdefault( + plugin_name, {} + ) + plugin_changes.update(plugin_value) + + item_values.update(item_changes) + + event_changes = [] + for item_id, item_changes in item_data_by_id.items(): + instance = self.get_instance_by_id(item_id) + event_changes.append({ + "instance": instance, + "changes": item_changes, + }) + + event_data = { + "changes": event_changes, + } + + self._emit_event( + VALUE_CHANGED_TOPIC, + event_data, + sender + ) + + def _bulk_pre_create_attrs_change_finished( + self, identifiers: List[str], sender: Optional[str] + ): + if not identifiers: + return + identifiers = list(set(identifiers)) + self._emit_event( + PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, + { + "identifiers": identifiers, + }, + sender, + ) + + def _bulk_create_attrs_change_finished( + self, instance_ids: List[str], sender: Optional[str] + ): + if not instance_ids: + return + + instances = [ + self.get_instance_by_id(instance_id) + for instance_id in set(instance_ids) + ] + self._emit_event( + CREATE_ATTR_DEFS_CHANGED_TOPIC, + { + "instances": instances, + }, + sender, + ) + + def _bulk_publish_attrs_change_finished( + self, + attr_info: Tuple[str, Union[str, None]], + sender: Optional[str], + ): + if not attr_info: + return + + instance_changes = {} + for instance_id, plugin_name in attr_info: + instance_data = instance_changes.setdefault( + instance_id, + { + "instance": None, + "plugin_names": set(), + } + ) + instance = self.get_instance_by_id(instance_id) + instance_data["instance"] = instance + instance_data["plugin_names"].add(plugin_name) + + self._emit_event( + PUBLISH_ATTR_DEFS_CHANGED_TOPIC, + {"instance_changes": instance_changes}, + sender, + ) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 8d7ede1fa6..fe41d2fe65 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -831,6 +831,16 @@ class Creator(BaseCreator): """ return self.pre_create_attr_defs + def _pre_create_attr_defs_changed(self): + """Called when pre-create attribute definitions change. + + Create plugin can call this method when knows that + 'get_pre_create_attr_defs' should be called again. + """ + self.create_context.create_plugin_pre_create_attr_defs_changed( + self.identifier + ) + class HiddenCreator(BaseCreator): @abstractmethod From cef35485eec06746ba4bbee520ea1b55a10c1974 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:23:05 +0200 Subject: [PATCH 069/546] fix variable name --- .../tools/publisher/widgets/product_attributes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 6478748f7a..7372e66efe 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -215,7 +215,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._on_instance_value_change ) - self._instance_ids = set() + self._current_instance_ids = set() self._context_selected = False self._main_layout = main_layout @@ -241,7 +241,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): def set_current_instances(self, instance_ids, context_selected): """Set current instances for which are attribute definitions shown.""" - self._instance_ids = set(instance_ids) + self._current_instance_ids = set(instance_ids) self._context_selected = context_selected self._refresh_content() @@ -259,7 +259,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._attr_def_id_to_plugin_name = {} result = self._controller.get_publish_attribute_definitions( - self._instance_ids, self._context_selected + self._current_instance_ids, self._context_selected ) content_widget = QtWidgets.QWidget(self._scroll_area) @@ -359,7 +359,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): for instance_id in event.data: if ( instance_id is None and self._context_selected - or instance_id in self._instance_ids + or instance_id in self._current_instance_ids ): self._refresh_content() break From 7537cf8ebc2f5733c6bc69f155ca267c668882f7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 17:45:48 +0200 Subject: [PATCH 070/546] update pre-create attributes on change --- client/ayon_core/pipeline/create/context.py | 5 ++++ client/ayon_core/tools/publisher/abstract.py | 20 +++++++++++++ .../tools/publisher/models/create.py | 28 ++++++++++++++++- .../tools/publisher/widgets/create_widget.py | 30 ++++++++++++++----- 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 9cde82db10..3245b68699 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -801,6 +801,11 @@ class CreateContext: def listen_to_value_changes(self, callback): self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback) + def listen_to_pre_create_attr_defs_change(self, callback): + self._event_hub.add_callback( + PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, callback + ) + def listen_to_create_attr_defs_change(self, callback): self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 9a81fa0ac0..bc72947551 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -265,6 +265,11 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): """ pass + @abstractmethod + def get_folder_id_from_path(self, folder_path: str) -> Optional[str]: + """Get folder id from folder path.""" + pass + # --- Create --- @abstractmethod def get_creator_items(self) -> Dict[str, "CreatorItem"]: @@ -276,6 +281,21 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): """ pass + @abstractmethod + def get_creator_item_by_id( + self, identifier: str + ) -> Optional["CreatorItem"]: + """Get creator item by identifier. + + Args: + identifier (str): Create plugin identifier. + + Returns: + Optional[CreatorItem]: Creator item or None. + + """ + pass + @abstractmethod def get_creator_icon( self, identifier: str diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index a2530cacc6..f6f9789514 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -374,6 +374,9 @@ class CreateModel: self._create_context.listen_to_value_changes( self._cc_value_changed ) + self._create_context.listen_to_pre_create_attr_defs_change( + self._cc_pre_create_attr_changed + ) self._create_context.listen_to_create_attr_defs_change( self._cc_create_attr_changed ) @@ -386,7 +389,7 @@ class CreateModel: def get_creator_items(self) -> Dict[str, CreatorItem]: """Creators that can be shown in create dialog.""" if self._creator_items is None: - self._creator_items = self._collect_creator_items() + self._refresh_creator_items() return self._creator_items def get_creator_item_by_id( @@ -883,6 +886,21 @@ class CreateModel: return output + def _refresh_creator_items(self, identifiers=None): + if identifiers is None: + self._creator_items = self._collect_creator_items() + return + + for identifier in identifiers: + if identifier not in self._creator_items: + continue + creator = self._create_context.creators.get(identifier) + if creator is None: + continue + self._creator_items[identifier] = ( + CreatorItem.from_creator(creator) + ) + def _cc_added_instance(self, event): instance_ids = { instance.id @@ -919,6 +937,14 @@ class CreateModel: {"instance_changes": instance_changes}, ) + def _cc_pre_create_attr_changed(self, event): + identifiers = event["identifiers"] + self._refresh_creator_items(identifiers) + self._emit_event( + "create.context.pre.create.attrs.changed", + {"identifiers": identifiers}, + ) + def _cc_create_attr_changed(self, event): instance_ids = { instance.id diff --git a/client/ayon_core/tools/publisher/widgets/create_widget.py b/client/ayon_core/tools/publisher/widgets/create_widget.py index 4c94c5c9b9..aecea2ec44 100644 --- a/client/ayon_core/tools/publisher/widgets/create_widget.py +++ b/client/ayon_core/tools/publisher/widgets/create_widget.py @@ -111,7 +111,7 @@ class CreateWidget(QtWidgets.QWidget): self._folder_path = None self._product_names = None - self._selected_creator = None + self._selected_creator_identifier = None self._prereq_available = False @@ -262,6 +262,10 @@ class CreateWidget(QtWidgets.QWidget): controller.register_event_callback( "controller.reset.finished", self._on_controler_reset ) + controller.register_event_callback( + "create.context.pre.create.attrs.changed", + self._pre_create_attr_changed + ) self._main_splitter_widget = main_splitter_widget @@ -512,6 +516,15 @@ class CreateWidget(QtWidgets.QWidget): # Trigger refresh only if is visible self.refresh() + def _pre_create_attr_changed(self, event): + if ( + self._selected_creator_identifier is None + or self._selected_creator_identifier not in event["identifiers"] + ): + return + + self._set_creator_by_identifier(self._selected_creator_identifier) + def _on_folder_change(self): self._refresh_product_name() if self._context_change_is_enabled(): @@ -563,12 +576,13 @@ class CreateWidget(QtWidgets.QWidget): self._set_creator_detailed_text(creator_item) self._pre_create_widget.set_creator_item(creator_item) - self._selected_creator = creator_item - if not creator_item: + self._selected_creator_identifier = None self._set_context_enabled(False) return + self._selected_creator_identifier = creator_item.identifier + if ( creator_item.create_allow_context_change != self._context_change_is_enabled() @@ -603,7 +617,7 @@ class CreateWidget(QtWidgets.QWidget): return # This should probably never happen? - if not self._selected_creator: + if not self._selected_creator_identifier: if self.product_name_input.text(): self.product_name_input.setText("") return @@ -625,11 +639,13 @@ class CreateWidget(QtWidgets.QWidget): folder_path = self._get_folder_path() task_name = self._get_task_name() - creator_idenfier = self._selected_creator.identifier # Calculate product name with Creator plugin try: product_name = self._controller.get_product_name( - creator_idenfier, variant_value, task_name, folder_path + self._selected_creator_identifier, + variant_value, + task_name, + folder_path ) except TaskNotSetError: self._create_btn.setEnabled(False) @@ -755,7 +771,7 @@ class CreateWidget(QtWidgets.QWidget): ) if success: - self._set_creator(self._selected_creator) + self._set_creator_by_identifier(self._selected_creator_identifier) self._variant_widget.setText(variant) self._controller.emit_card_message("Creation finished...") self._last_thumbnail_path = None From b35055c3c7e6c0e77c32624d94b3004bad22d2ef Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:01:16 +0200 Subject: [PATCH 071/546] added method to update context --- client/ayon_core/tools/publisher/abstract.py | 6 ++++++ client/ayon_core/tools/publisher/control.py | 5 +++++ client/ayon_core/tools/publisher/models/create.py | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index bc72947551..3a968eee28 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -347,6 +347,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def set_instances_context_info( + self, changes_by_instance_id: Dict[str, Dict[str, Any]] + ): + pass + @abstractmethod def get_existing_product_names(self, folder_path: str) -> List[str]: pass diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 8664cfe605..43b491a20f 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -195,6 +195,11 @@ class PublisherController( def get_instances_context_info(self, instance_ids=None): return self._create_model.get_instances_context_info(instance_ids) + def set_instances_context_info(self, changes_by_instance_id): + return self._create_model.set_instances_context_info( + changes_by_instance_id + ) + def get_convertor_items(self): return self._create_model.get_convertor_items() diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index f6f9789514..efc761a407 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -447,6 +447,13 @@ class CreateModel: instances ) + def set_instances_context_info(self, changes_by_instance_id): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id, changes in changes_by_instance_id.items(): + instance = self._get_instance_by_id(instance_id) + for key, value in changes.items(): + instance[key] = value + def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id From f7dc16d9bfe9be0ce38a0d915d3c7170244aeaf8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:01:34 +0200 Subject: [PATCH 072/546] update context on instance on context change --- .../publisher/widgets/product_context.py | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index b66d67717c..977f5eccb3 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -632,7 +632,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): super().__init__(parent) self._controller: AbstractPublisherFrontend = controller - self._current_instances = [] + self._current_instances_by_id = {} variant_input = VariantInputWidget(self) folder_value_widget = FoldersFields(controller, self) @@ -678,6 +678,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget): submit_btn.clicked.connect(self._on_submit) cancel_btn.clicked.connect(self._on_cancel) + controller.register_event_callback( + "create.context.value.changed", + self._on_instance_value_change + ) + self.variant_input = variant_input self.folder_value_widget = folder_value_widget self.task_value_widget = task_value_widget @@ -704,21 +709,26 @@ class GlobalAttrsWidget(QtWidgets.QWidget): product_names = set() invalid_tasks = False folder_paths = [] - for instance in self._current_instances: + changes_by_id = {} + for instance in self._current_instances_by_id.values(): # Ignore instances that have promised context if instance.has_promised_context: continue + instance_changes = {} new_variant_value = instance.variant new_folder_path = instance.folder_path new_task_name = instance.task_name if variant_value is not None: + instance_changes["variant"] = variant_value new_variant_value = variant_value if folder_path is not None: + instance_changes["folderPath"] = folder_path new_folder_path = folder_path if task_name is not None: + instance_changes["task"] = task_name new_task_name = task_name folder_paths.append(new_folder_path) @@ -747,6 +757,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance.task_name = task_name or None instance.product_name = new_product_name + if instance.product_name != new_product_name: + instance_changes["productName"] = new_product_name + changes_by_id[instance.id] = instance_changes if invalid_tasks: self.task_value_widget.set_invalid_empty_task() @@ -765,6 +778,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if task_name is not None: self.task_value_widget.confirm_value(folder_paths) + self._controller.set_instances_context_info(changes_by_id) self.instance_context_changed.emit() def _on_cancel(self): @@ -818,20 +832,25 @@ class GlobalAttrsWidget(QtWidgets.QWidget): """ self._set_btns_visible(False) - self._current_instances = instances + self._current_instances_by_id = { + instance.id: instance + for instance in instances + } + self._refresh_content() + def _refresh_content(self): folder_paths = set() variants = set() product_types = set() product_names = set() editable = True - if len(instances) == 0: + if len(self._current_instances_by_id) == 0: editable = False folder_task_combinations = [] context_editable = None - for instance in instances: + for instance in self._current_instances_by_id.values(): if not instance.has_promised_context: context_editable = True elif context_editable is None: @@ -879,3 +898,31 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self.folder_value_widget.setToolTip(folder_tooltip) self.task_value_widget.setToolTip(task_tooltip) + + def _on_instance_value_change(self, event): + if not self._current_instances_by_id: + return + + changed = False + for instance_id, changes in event["instance_changes"].items(): + instance = self._current_instances_by_id.get(instance_id) + if instance is None: + continue + + for key, attr_name in ( + ("folderPath", "folder_path"), + ("task", "task_name"), + ("variant", "variant"), + ("productType", "product_type"), + ("productName", "product_name"), + ): + if key in changes: + setattr(instance, attr_name, changes[key]) + changed = True + break + if changed: + break + + if changed: + self._refresh_content() + self.instance_context_changed.emit() From 1a64490256063dc33fd9e9990329e5182b339838 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 00:26:39 +0200 Subject: [PATCH 073/546] Allow representation switch on update if representation does not exist (e.g. `ma` -> `mb` representation) --- client/ayon_core/pipeline/load/plugins.py | 20 +++++++++ client/ayon_core/pipeline/load/utils.py | 49 ++++++++++++++++------- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 2475800cbb..28a7e775d6 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -242,6 +242,26 @@ class LoaderPlugin(list): if hasattr(self, "_fname"): return self._fname + def update_allowed_representation_switches(self): + """Return a mapping from source representation names to ordered + destination representation names to which switching is allowed if + the source representation name does not exist for the new version. + + For example, to allow an automated switch on update from representation + `ma` to `mb` or `abc` if the new version does not have a `ma` + representation you can return: + {"ma": ["mb", "abc"]} + + The order of the names in the returned values is important, because + if `ma` is missing and both of the replacement representations are + present than the first one will be chosen. + + Returns: + Dict[str, List[str]]: Mapping from representation names to allowed + alias representation names switching to is allowed on update. + """ + return {} + class ProductLoaderPlugin(LoaderPlugin): """Load product into host application diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 9ba407193e..bb74194ea1 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -505,21 +505,6 @@ def update_container(container, version=-1): project_name, product_entity["folderId"] ) - repre_name = current_representation["name"] - new_representation = ayon_api.get_representation_by_name( - project_name, repre_name, new_version["id"] - ) - if new_representation is None: - raise ValueError( - "Representation '{}' wasn't found on requested version".format( - repre_name - ) - ) - - path = get_representation_path(new_representation) - if not path or not os.path.exists(path): - raise ValueError("Path {} doesn't exist".format(path)) - # Run update on the Loader for this container Loader = _get_container_loader(container) if not Loader: @@ -527,6 +512,40 @@ def update_container(container, version=-1): "Can't update container because loader '{}' was not found." .format(container.get("loader")) ) + + repre_name = current_representation["name"] + new_representation = ayon_api.get_representation_by_name( + project_name, repre_name, new_version["id"] + ) + if new_representation is None: + # The representation name is not found in the new version. + # Allow updating to a 'matching' representation if the loader + # has defined compatible update conversions + mapping = Loader.update_allowed_representation_switches() + switch_repre_names = mapping.get(repre_name) + if switch_repre_names: + representations = ayon_api.get_representations( + project_name, + representation_names=switch_repre_names, + version_ids=[new_version["id"]]) + representations_by_name = { + repre["name"]: repre for repre in representations + } + for name in switch_repre_names: + if name in representations_by_name: + new_representation = representations_by_name[name] + break + + if new_representation is None: + raise ValueError( + "Representation '{}' wasn't found on requested version".format( + repre_name + ) + ) + + path = get_representation_path(new_representation) + if not path or not os.path.exists(path): + raise ValueError("Path {} doesn't exist".format(path)) project_entity = ayon_api.get_project(project_name) context = { "project": project_entity, From 1dbaa57e0926fabc6afa9b85a5ae3e7856dccb07 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 00:39:04 +0200 Subject: [PATCH 074/546] Fix call to method --- client/ayon_core/pipeline/load/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index bb74194ea1..b258d20a3d 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -521,7 +521,7 @@ def update_container(container, version=-1): # The representation name is not found in the new version. # Allow updating to a 'matching' representation if the loader # has defined compatible update conversions - mapping = Loader.update_allowed_representation_switches() + mapping = Loader().update_allowed_representation_switches() switch_repre_names = mapping.get(repre_name) if switch_repre_names: representations = ayon_api.get_representations( From e20671ddf7bacaa14bd4d9903af1039244fe3dbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Oct 2024 17:48:53 +0200 Subject: [PATCH 075/546] simplified product context widget --- .../publisher/widgets/product_context.py | 57 +++++++++---------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index 977f5eccb3..9fdf55c3f9 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -710,15 +710,15 @@ class GlobalAttrsWidget(QtWidgets.QWidget): invalid_tasks = False folder_paths = [] changes_by_id = {} - for instance in self._current_instances_by_id.values(): + for item in self._current_instances_by_id.values(): # Ignore instances that have promised context - if instance.has_promised_context: + if item.has_promised_context: continue instance_changes = {} - new_variant_value = instance.variant - new_folder_path = instance.folder_path - new_task_name = instance.task_name + new_variant_value = item.variant + new_folder_path = item.folder_path + new_task_name = item.task_name if variant_value is not None: instance_changes["variant"] = variant_value new_variant_value = variant_value @@ -734,32 +734,22 @@ class GlobalAttrsWidget(QtWidgets.QWidget): folder_paths.append(new_folder_path) try: new_product_name = self._controller.get_product_name( - instance.creator_identifier, + item.creator_identifier, new_variant_value, new_task_name, new_folder_path, - instance.id, + item.id, ) except TaskNotSetError: invalid_tasks = True - product_names.add(instance.product_name) + product_names.add(item.product_name) continue product_names.add(new_product_name) - if variant_value is not None: - instance.variant = variant_value - - if folder_path is not None: - instance.folder_path = folder_path - - if task_name is not None: - instance.task_name = task_name or None - - instance.product_name = new_product_name - if instance.product_name != new_product_name: + if item.product_name != new_product_name: instance_changes["productName"] = new_product_name - changes_by_id[instance.id] = instance_changes + changes_by_id[item.id] = instance_changes if invalid_tasks: self.task_value_widget.set_invalid_empty_task() @@ -838,6 +828,12 @@ class GlobalAttrsWidget(QtWidgets.QWidget): } self._refresh_content() + def _refresh_items(self): + instance_ids = set(self._current_instances_by_id.keys()) + self._current_instances_by_id = ( + self._controller.get_instance_items_by_id(instance_ids) + ) + def _refresh_content(self): folder_paths = set() variants = set() @@ -850,23 +846,23 @@ class GlobalAttrsWidget(QtWidgets.QWidget): folder_task_combinations = [] context_editable = None - for instance in self._current_instances_by_id.values(): - if not instance.has_promised_context: + for item in self._current_instances_by_id.values(): + if not item.has_promised_context: context_editable = True elif context_editable is None: context_editable = False # NOTE I'm not sure how this can even happen? - if instance.creator_identifier is None: + if item.creator_identifier is None: editable = False - variants.add(instance.variant or self.unknown_value) - product_types.add(instance.product_type or self.unknown_value) - folder_path = instance.folder_path or self.unknown_value - task_name = instance.task_name or "" + variants.add(item.variant or self.unknown_value) + product_types.add(item.product_type or self.unknown_value) + folder_path = item.folder_path or self.unknown_value + task_name = item.task_name or "" folder_paths.add(folder_path) folder_task_combinations.append((folder_path, task_name)) - product_names.add(instance.product_name or self.unknown_value) + product_names.add(item.product_name or self.unknown_value) if not editable: context_editable = False @@ -905,8 +901,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): changed = False for instance_id, changes in event["instance_changes"].items(): - instance = self._current_instances_by_id.get(instance_id) - if instance is None: + if instance_id not in self._current_instances_by_id: continue for key, attr_name in ( @@ -917,12 +912,12 @@ class GlobalAttrsWidget(QtWidgets.QWidget): ("productName", "product_name"), ): if key in changes: - setattr(instance, attr_name, changes[key]) changed = True break if changed: break if changed: + self._refresh_items() self._refresh_content() self.instance_context_changed.emit() From 73eaa0711645051344d05180f6024ac978660e8e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Oct 2024 19:18:43 +0200 Subject: [PATCH 076/546] fix context change --- .../publisher/widgets/product_context.py | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index 9fdf55c3f9..f11dc90a5d 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -633,6 +633,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self._controller: AbstractPublisherFrontend = controller self._current_instances_by_id = {} + self._invalid_task_item_ids = set() variant_input = VariantInputWidget(self) folder_value_widget = FoldersFields(controller, self) @@ -728,8 +729,8 @@ class GlobalAttrsWidget(QtWidgets.QWidget): new_folder_path = folder_path if task_name is not None: - instance_changes["task"] = task_name - new_task_name = task_name + instance_changes["task"] = task_name or None + new_task_name = task_name or None folder_paths.append(new_folder_path) try: @@ -740,8 +741,10 @@ class GlobalAttrsWidget(QtWidgets.QWidget): new_folder_path, item.id, ) + self._invalid_task_item_ids.discard(item.id) except TaskNotSetError: + self._invalid_task_item_ids.add(item.id) invalid_tasks = True product_names.add(item.product_name) continue @@ -749,7 +752,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): product_names.add(new_product_name) if item.product_name != new_product_name: instance_changes["productName"] = new_product_name - changes_by_id[item.id] = instance_changes + + if instance_changes: + changes_by_id[item.id] = instance_changes if invalid_tasks: self.task_value_widget.set_invalid_empty_task() @@ -769,6 +774,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self.task_value_widget.confirm_value(folder_paths) self._controller.set_instances_context_info(changes_by_id) + self._refresh_items() self.instance_context_changed.emit() def _on_cancel(self): @@ -826,6 +832,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): instance.id: instance for instance in instances } + self._invalid_task_item_ids = set() self._refresh_content() def _refresh_items(self): @@ -846,11 +853,14 @@ class GlobalAttrsWidget(QtWidgets.QWidget): folder_task_combinations = [] context_editable = None + invalid_tasks = False for item in self._current_instances_by_id.values(): if not item.has_promised_context: context_editable = True elif context_editable is None: context_editable = False + if item.id in self._invalid_task_item_ids: + invalid_tasks = True # NOTE I'm not sure how this can even happen? if item.creator_identifier is None: @@ -882,6 +892,9 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self.folder_value_widget.setEnabled(context_editable) self.task_value_widget.setEnabled(context_editable) + if invalid_tasks: + self.task_value_widget.set_invalid_empty_task() + if not editable: folder_tooltip = "Select instances to change folder path." task_tooltip = "Select instances to change task name." @@ -905,11 +918,11 @@ class GlobalAttrsWidget(QtWidgets.QWidget): continue for key, attr_name in ( - ("folderPath", "folder_path"), - ("task", "task_name"), - ("variant", "variant"), - ("productType", "product_type"), - ("productName", "product_name"), + "folderPath", + "task", + "variant", + "productType", + "productName", ): if key in changes: changed = True From 266140ad40f7a651244220533257eda57aa305ba Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 2 Oct 2024 19:23:37 +0200 Subject: [PATCH 077/546] Refactor function `update_allowed_representation_switches` -> `get_representation_name_aliases` --- client/ayon_core/pipeline/load/plugins.py | 26 +++++++++++------------ client/ayon_core/pipeline/load/utils.py | 9 ++++---- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 28a7e775d6..1fb906fd65 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -242,25 +242,25 @@ class LoaderPlugin(list): if hasattr(self, "_fname"): return self._fname - def update_allowed_representation_switches(self): - """Return a mapping from source representation names to ordered - destination representation names to which switching is allowed if - the source representation name does not exist for the new version. + @classmethod + def get_representation_name_aliases(cls, representation_name: str): + """Return representation names to which switching is allowed from + the input representation name, like an alias replacement of the input + `representation_name`. For example, to allow an automated switch on update from representation - `ma` to `mb` or `abc` if the new version does not have a `ma` - representation you can return: - {"ma": ["mb", "abc"]} + `ma` to `mb` or `abc`, then when `representation_name` is `ma` return: + ["mb", "abc"] - The order of the names in the returned values is important, because - if `ma` is missing and both of the replacement representations are - present than the first one will be chosen. + The order of the names in the returned representation names is + important, because the first one existing under the new version will + be chosen. Returns: - Dict[str, List[str]]: Mapping from representation names to allowed - alias representation names switching to is allowed on update. + List[str]: Representation names switching to is allowed on update + if the input representation name is not found on the new version. """ - return {} + return [] class ProductLoaderPlugin(LoaderPlugin): diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index b258d20a3d..ee2c1af07f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -521,17 +521,16 @@ def update_container(container, version=-1): # The representation name is not found in the new version. # Allow updating to a 'matching' representation if the loader # has defined compatible update conversions - mapping = Loader().update_allowed_representation_switches() - switch_repre_names = mapping.get(repre_name) - if switch_repre_names: + repre_name_aliases = Loader.get_representation_name_aliases(repre_name) + if repre_name_aliases: representations = ayon_api.get_representations( project_name, - representation_names=switch_repre_names, + representation_names=repre_name_aliases, version_ids=[new_version["id"]]) representations_by_name = { repre["name"]: repre for repre in representations } - for name in switch_repre_names: + for name in repre_name_aliases: if name in representations_by_name: new_representation = representations_by_name[name] break From 578618c1e317295a411fbf9f79a45bf456980319 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 11:28:28 +0200 Subject: [PATCH 078/546] added method allowing to register to callbacks --- client/ayon_core/pipeline/create/context.py | 3 +++ .../pipeline/publish/publish_plugins.py | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3245b68699..a09bf52d79 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -667,6 +667,9 @@ class CreateContext: if plugin not in plugins_by_targets ] + for plugin in plugins_with_defs: + plugin.register_create_context_callbacks(self) + self.publish_plugins_mismatch_targets = plugins_mismatch_targets self.publish_discover_result = discover_result self.publish_plugins = plugins_by_targets diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 59547ce651..c9732e4928 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -126,6 +126,27 @@ class AYONPyblishPluginMixin: # for callback in self._state_change_callbacks: # callback(self) + @classmethod + def register_create_context_callbacks(cls, create_context): + """Register callbacks for create context. + + It is possible to register callbacks listening to changes happened + in create context. + + Methods available on create context: + - listen_to_added_instances + - listen_to_removed_instances + - listen_to_value_changes + - listen_to_pre_create_attr_defs_change + - listen_to_create_attr_defs_change + - listen_to_publish_attr_defs_change + + Args: + create_context (CreateContext): Create context. + + """ + pass + @classmethod def get_attribute_defs(cls): """Publish attribute definitions. From 941836cc0daff058afcaf756e97a219a5171736a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:26:37 +0200 Subject: [PATCH 079/546] removed unused import --- client/ayon_core/pipeline/create/structures.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 2bbd6dabc5..1142ba84eb 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -6,7 +6,6 @@ from typing import Optional, Dict, List, Any from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, UnknownDef, - UIDef, serialize_attr_defs, deserialize_attr_defs, ) From 7f73d9c9e4857aefe6ca05772229db106c64bc98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:26:53 +0200 Subject: [PATCH 080/546] safe 'register_create_context_callbacks' --- client/ayon_core/pipeline/create/context.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a09bf52d79..bc22604bca 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -668,7 +668,21 @@ class CreateContext: ] for plugin in plugins_with_defs: - plugin.register_create_context_callbacks(self) + if not inspect.ismethod(plugin.register_create_context_callbacks): + self.log.warning( + f"Plugin {plugin.__name__} does not have" + f" 'register_create_context_callbacks'" + f" defined as class method." + ) + continue + try: + plugin.register_create_context_callbacks(self) + except Exception: + self.log.error( + f"Failed to register callbacks for plugin" + f" {plugin.__name__}.", + exc_info=True + ) self.publish_plugins_mismatch_targets = plugins_mismatch_targets self.publish_discover_result = discover_result From b535dcb81f912a7c7ba29299417afa03da2c6c66 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:14:41 +0200 Subject: [PATCH 081/546] added docstrings to listen methods --- client/ayon_core/pipeline/create/context.py | 151 +++++++++++++++++++- 1 file changed, 150 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index bc22604bca..a08cd3624d 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -667,6 +667,7 @@ class CreateContext: if plugin not in plugins_by_targets ] + # Register create context callbacks for plugin in plugins_with_defs: if not inspect.ismethod(plugin.register_create_context_callbacks): self.log.warning( @@ -810,23 +811,171 @@ class CreateContext: ) def listen_to_added_instances(self, callback): - self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback) + """Register callback for added instances. + + Event is triggered when instances are already available in context + and have set create/publish attribute definitions. + + Data structure of event:: + + ```python + { + "instances": [CreatedInstance, ...] + } + ``` + + Args: + callback (Callable): Callback function that will be called when + instances are added to context. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ + return self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback) def listen_to_removed_instances(self, callback): + """Register callback for removed instances. + + Event is triggered when instances are already removed from context. + + Data structure of event:: + + ```python + { + "instances": [CreatedInstance, ...] + } + ``` + + Args: + callback (Callable): Callback function that will be called when + instances are removed from context. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback) def listen_to_value_changes(self, callback): + """Register callback to listen value changes. + + Event is triggered when any value changes on any instance or + context data. + + Data structure of event:: + + ```python + { + "changes": [ + { + "instance": CreatedInstance, + "changes": { + "folderPath": "/new/folder/path", + "creator_attributes": { + "attr_1": "value_1" + } + } + } + ] + } + ``` + + Args: + callback (Callable): Callback function that will be called when + value changed. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback) def listen_to_pre_create_attr_defs_change(self, callback): + """Register callback to listen pre-create attribute changes. + + Create plugin can trigger refresh of pre-create attributes. Usage of + this event is mainly for publisher UI. + + Data structure of event:: + + ```python + { + "identifiers": ["create_plugin_identifier"] + } + ``` + + Args: + callback (Callable): Callback function that will be called when + pre-create attributes should be refreshed. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ self._event_hub.add_callback( PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, callback ) def listen_to_create_attr_defs_change(self, callback): + """Register callback to listen create attribute changes. + + Create plugin changed attribute definitions of instance. + + Data structure of event:: + + ```python + { + "instances": [CreatedInstance, ...] + } + ``` + + Args: + callback (Callable): Callback function that will be called when + create attributes changed. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback) def listen_to_publish_attr_defs_change(self, callback): + """Register callback to listen publish attribute changes. + + Publish plugin changed attribute definitions of instance of context. + + Data structure of event:: + + ```python + { + "instance_changes": { + None: { + "instance": None, + "plugin_names": {"PluginA"}, + } + "": { + "instance": CreatedInstance, + "plugin_names": {"PluginB", "PluginC"}, + } + } + } + ``` + + Args: + callback (Callable): Callback function that will be called when + publish attributes changed. + + Returns: + EventCallback: Created callback object which can be used to + stop listening. + + """ self._event_hub.add_callback( PUBLISH_ATTR_DEFS_CHANGED_TOPIC, callback ) From 5d1e4863e0005de4edbe0cfadfaac50ee247c0cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 3 Oct 2024 15:21:02 +0200 Subject: [PATCH 082/546] added some docstrings --- .../ayon_core/pipeline/create/structures.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 1142ba84eb..68b8d7ecf1 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -734,6 +734,17 @@ class CreatedInstance: return output def update_create_attr_defs(self, attr_defs, value=None): + """Create plugin updates create attribute definitions. + + Method called by create plugin when attribute definitions should + be changed. + + Args: + attr_defs (List[AbstractAttrDef]): Attribute definitions. + value (Optional[Dict[str, Any]]): Values of attribute definitions. + Current values are used if not passed in. + + """ if value is None: value = self._data["creator_attributes"] @@ -780,6 +791,13 @@ class CreatedInstance: ) def attribute_value_changed(self, key, changes): + """A value changed. + + Args: + key (str): Key of attribute values. + changes (Dict[str, Any]): Changes in values. + + """ self._create_context.instance_values_changed(self.id, {key: changes}) def set_publish_plugin_attr_defs(self, plugin_name, attr_defs): @@ -798,6 +816,13 @@ class CreatedInstance: ) def publish_attribute_value_changed(self, plugin_name, value): + """Method called from PublishAttributes. + + Args: + plugin_name (str): Plugin name. + value (Dict[str, Any]): Changes in values for the plugin. + + """ self._create_context.instance_values_changed( self.id, { @@ -816,4 +841,10 @@ class CreatedInstance: @property def _create_context(self): + """Get create context. + + Returns: + CreateContext: Context object which wraps object. + + """ return self._creator.create_context From 58cc97ebfd0d576540f2c7a814eeb791225c6339 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:37:52 +0200 Subject: [PATCH 083/546] added information about 'create_context' in event callback --- client/ayon_core/pipeline/create/context.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a08cd3624d..0ea69d3173 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -820,7 +820,8 @@ class CreateContext: ```python { - "instances": [CreatedInstance, ...] + "instances": [CreatedInstance, ...], + "create_context": CreateContext } ``` @@ -844,7 +845,8 @@ class CreateContext: ```python { - "instances": [CreatedInstance, ...] + "instances": [CreatedInstance, ...], + "create_context": CreateContext } ``` @@ -879,7 +881,8 @@ class CreateContext: } } } - ] + ], + "create_context": CreateContext } ``` @@ -904,7 +907,8 @@ class CreateContext: ```python { - "identifiers": ["create_plugin_identifier"] + "identifiers": ["create_plugin_identifier"], + "create_context": CreateContext } ``` @@ -930,7 +934,8 @@ class CreateContext: ```python { - "instances": [CreatedInstance, ...] + "instances": [CreatedInstance, ...], + "create_context": CreateContext } ``` @@ -963,7 +968,8 @@ class CreateContext: "instance": CreatedInstance, "plugin_names": {"PluginB", "PluginC"}, } - } + }, + "create_context": CreateContext } ``` From 652d17113bad063cca059c716267bd250f4df812 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:38:02 +0200 Subject: [PATCH 084/546] added method to update context publish attributes --- client/ayon_core/pipeline/create/context.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0ea69d3173..657f71aa97 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1002,6 +1002,21 @@ class CreateContext: self._original_context_data, self.context_data_to_store() ) + def set_context_publish_plugin_attr_defs(self, plugin_name, attr_defs): + """Set attribute definitions for CreateContext publish plugin. + + Args: + plugin_name(str): Name of publish plugin. + attr_defs(List[AbstractAttrDef]): Attribute definitions. + + """ + self.publish_attributes.set_publish_plugin_attr_defs( + plugin_name, attr_defs + ) + self.instance_publish_attr_defs_changed( + None, plugin_name + ) + def creator_adds_instance(self, instance: "CreatedInstance"): """Creator adds new instance to context. From 36b54c0b4b9d04eb9ed47b8beab12e2b07a4164a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:20:15 +0200 Subject: [PATCH 085/546] added helper method 'instance_matches_plugin_families' --- .../pipeline/publish/publish_plugins.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index c9732e4928..e755236cf8 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -176,6 +176,26 @@ class AYONPyblishPluginMixin: return [] return cls.get_attribute_defs() + @classmethod + def instance_matches_plugin_families(cls, instance): + """Check if instance matches families. + + Args: + instance (CreatedInstance): Instance to check. + + Returns: + bool: True if instance matches plugin families. + + """ + if not cls.__instanceEnabled__: + return False + + for _ in pyblish.logic.plugins_by_families( + [cls], [instance.product_type] + ): + return True + return False + @classmethod def get_attribute_defs_for_instance(cls, create_context, instance): """Publish attribute definitions for an instance. @@ -191,14 +211,9 @@ class AYONPyblishPluginMixin: list[AbstractAttrDef]: Attribute definitions for plugin. """ - if not cls.__instanceEnabled__: + if not cls.instance_matches_plugin_families(instance): return [] - - for _ in pyblish.logic.plugins_by_families( - [cls], [instance.product_type] - ): - return cls.get_attribute_defs() - return [] + return cls.get_attribute_defs() @classmethod def convert_attribute_values(cls, create_context, instance): From 5cb71523857c073d4b4f444e4f972f7d4e9edd42 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:40:18 +0200 Subject: [PATCH 086/546] allow to use the method for context plugins --- client/ayon_core/pipeline/publish/publish_plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index e755236cf8..cef3bb8bea 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -181,14 +181,15 @@ class AYONPyblishPluginMixin: """Check if instance matches families. Args: - instance (CreatedInstance): Instance to check. + instance (Optional[CreatedInstance]): Instance to check. Or None + for context. Returns: bool: True if instance matches plugin families. """ if not cls.__instanceEnabled__: - return False + return instance is None for _ in pyblish.logic.plugins_by_families( [cls], [instance.product_type] From d962e01c53e299faaa8c7b168e705d70003f7563 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:55:54 +0200 Subject: [PATCH 087/546] fix attribute error --- client/ayon_core/pipeline/publish/publish_plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index cef3bb8bea..4db289e9ba 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -188,8 +188,11 @@ class AYONPyblishPluginMixin: bool: True if instance matches plugin families. """ + if instance is None: + return not cls.__instanceEnabled__ + if not cls.__instanceEnabled__: - return instance is None + return False for _ in pyblish.logic.plugins_by_families( [cls], [instance.product_type] From 0348e9ea2224e32bb860048d1787b49ded6872c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:50:00 +0200 Subject: [PATCH 088/546] implemented 'get' in 'PublishAttributes' --- client/ayon_core/pipeline/create/structures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 68b8d7ecf1..32aac6562e 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -271,6 +271,9 @@ class PublishAttributes: def items(self): return self._data.items() + def get(self, key, default=None): + return self._data.get(key, default) + def pop(self, key, default=None): """Remove or reset value for plugin. From 03560f8ff6b772466f8ab628a1a91d4cc6f41880 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:35:15 +0200 Subject: [PATCH 089/546] unify method name --- client/ayon_core/pipeline/create/context.py | 2 +- client/ayon_core/pipeline/publish/publish_plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 657f71aa97..e4f462a9c5 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1251,7 +1251,7 @@ class CreateContext: for plugin in self.plugins_with_defs: attr_defs = None try: - attr_defs = plugin.get_attribute_defs_for_instance( + attr_defs = plugin.get_attr_defs_for_instance( self, instance ) except Exception: diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 4db289e9ba..139a585287 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -201,7 +201,7 @@ class AYONPyblishPluginMixin: return False @classmethod - def get_attribute_defs_for_instance(cls, create_context, instance): + def get_attr_defs_for_instance(cls, create_context, instance): """Publish attribute definitions for an instance. Attributes available for all families in plugin's `families` attribute. From d13990a604ce18cb0ed825956a35d65c06867a78 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:37:16 +0200 Subject: [PATCH 090/546] unify context method too --- client/ayon_core/pipeline/create/context.py | 2 +- client/ayon_core/pipeline/publish/publish_plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index e4f462a9c5..0d98fe28e2 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -803,7 +803,7 @@ class CreateContext: publish_attributes.update(output) for plugin in self.plugins_with_defs: - attr_defs = plugin.get_attribute_defs_for_context(self) + attr_defs = plugin.get_attr_defs_for_context (self) if not attr_defs: continue self._publish_attributes.set_publish_plugin_attr_defs( diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 139a585287..c65eeb73b4 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -160,7 +160,7 @@ class AYONPyblishPluginMixin: return [] @classmethod - def get_attribute_defs_for_context(cls, create_context): + def get_attr_defs_for_context (cls, create_context): """Publish attribute definitions for context. Attributes available for all families in plugin's `families` attribute. From 344ed86dc0350728bb967a8d83566d0f2fedfbd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:41:34 +0200 Subject: [PATCH 091/546] better name to change create attr defs --- client/ayon_core/pipeline/create/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 32aac6562e..9d7bd5387e 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -736,7 +736,7 @@ class CreatedInstance: return output - def update_create_attr_defs(self, attr_defs, value=None): + def set_create_attr_defs(self, attr_defs, value=None): """Create plugin updates create attribute definitions. Method called by create plugin when attribute definitions should From cd8a16cf88b000c29f2c1979329912a90cbb3e1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:46:19 +0200 Subject: [PATCH 092/546] fix used method name --- client/ayon_core/pipeline/create/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 9d7bd5387e..8594d82848 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -512,7 +512,7 @@ class CreatedInstance: self._data["instance_id"] = str(uuid4()) creator_attr_defs = creator.get_attr_defs_for_instance(self) - self.update_create_attr_defs( + self.set_create_attr_defs( creator_attr_defs, creator_values ) From 8af66a0f783a959ca477418618775fe9081f6574 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:05:12 +0200 Subject: [PATCH 093/546] replaced 'liste_to' prefix with 'add' prefix --- client/ayon_core/pipeline/create/context.py | 12 ++++++------ client/ayon_core/pipeline/publish/publish_plugins.py | 12 ++++++------ client/ayon_core/tools/publisher/models/create.py | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0d98fe28e2..adbb03a820 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -810,7 +810,7 @@ class CreateContext: plugin.__name__, attr_defs ) - def listen_to_added_instances(self, callback): + def add_instances_added_callback(self, callback): """Register callback for added instances. Event is triggered when instances are already available in context @@ -836,7 +836,7 @@ class CreateContext: """ return self._event_hub.add_callback(INSTANCE_ADDED_TOPIC, callback) - def listen_to_removed_instances(self, callback): + def add_instances_removed_callback (self, callback): """Register callback for removed instances. Event is triggered when instances are already removed from context. @@ -861,7 +861,7 @@ class CreateContext: """ self._event_hub.add_callback(INSTANCE_REMOVED_TOPIC, callback) - def listen_to_value_changes(self, callback): + def add_value_changed_callback(self, callback): """Register callback to listen value changes. Event is triggered when any value changes on any instance or @@ -897,7 +897,7 @@ class CreateContext: """ self._event_hub.add_callback(VALUE_CHANGED_TOPIC, callback) - def listen_to_pre_create_attr_defs_change(self, callback): + def add_pre_create_attr_defs_change_callback (self, callback): """Register callback to listen pre-create attribute changes. Create plugin can trigger refresh of pre-create attributes. Usage of @@ -925,7 +925,7 @@ class CreateContext: PRE_CREATE_ATTR_DEFS_CHANGED_TOPIC, callback ) - def listen_to_create_attr_defs_change(self, callback): + def add_create_attr_defs_change_callback (self, callback): """Register callback to listen create attribute changes. Create plugin changed attribute definitions of instance. @@ -950,7 +950,7 @@ class CreateContext: """ self._event_hub.add_callback(CREATE_ATTR_DEFS_CHANGED_TOPIC, callback) - def listen_to_publish_attr_defs_change(self, callback): + def add_publish_attr_defs_change_callback (self, callback): """Register callback to listen publish attribute changes. Publish plugin changed attribute definitions of instance of context. diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index c65eeb73b4..3c2bafdba3 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -134,12 +134,12 @@ class AYONPyblishPluginMixin: in create context. Methods available on create context: - - listen_to_added_instances - - listen_to_removed_instances - - listen_to_value_changes - - listen_to_pre_create_attr_defs_change - - listen_to_create_attr_defs_change - - listen_to_publish_attr_defs_change + - add_instances_added_callback + - add_instances_removed_callback + - add_value_changed_callback + - add_pre_create_attr_defs_change_callback + - add_create_attr_defs_change_callback + - add_publish_attr_defs_change_callback Args: create_context (CreateContext): Create context. diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index efc761a407..4b27081db2 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -365,22 +365,22 @@ class CreateModel: self._emit_event("create.model.reset") - self._create_context.listen_to_added_instances( + self._create_context.add_instances_added_callback( self._cc_added_instance ) - self._create_context.listen_to_removed_instances( + self._create_context.add_instances_removed_callback ( self._cc_removed_instance ) - self._create_context.listen_to_value_changes( + self._create_context.add_value_changed_callback( self._cc_value_changed ) - self._create_context.listen_to_pre_create_attr_defs_change( + self._create_context.add_pre_create_attr_defs_change_callback ( self._cc_pre_create_attr_changed ) - self._create_context.listen_to_create_attr_defs_change( + self._create_context.add_create_attr_defs_change_callback ( self._cc_create_attr_changed ) - self._create_context.listen_to_publish_attr_defs_change( + self._create_context.add_publish_attr_defs_change_callback ( self._cc_publish_attr_changed ) From a40d8cc8172b23b641ef8e5d88a86576d3f646d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:14:03 +0200 Subject: [PATCH 094/546] added typehint imports --- client/ayon_core/pipeline/publish/publish_plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 3c2bafdba3..118596bffb 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -1,5 +1,7 @@ import inspect from abc import ABCMeta +import typing + import pyblish.api import pyblish.logic from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin @@ -16,6 +18,8 @@ from ayon_core.pipeline.colorspace import ( get_colorspace_settings_from_publish_context, set_colorspace_data_to_representation ) +if typing.TYPE_CHECKING: + from ayon_core.pipeline.create import CreateContext, CreatedInstance class AbstractMetaInstancePlugin(ABCMeta, MetaPlugin): From 8a3a1e8042ce047fe908d2a56d109e17401be7b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 4 Oct 2024 18:14:12 +0200 Subject: [PATCH 095/546] moved imports a little --- client/ayon_core/pipeline/publish/publish_plugins.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 118596bffb..9124e8b763 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -5,8 +5,14 @@ import typing import pyblish.api import pyblish.logic from pyblish.plugin import MetaPlugin, ExplicitMetaPlugin + from ayon_core.lib import BoolDef +from ayon_core.pipeline.colorspace import ( + get_colorspace_settings_from_publish_context, + set_colorspace_data_to_representation +) + from .lib import ( load_help_content_from_plugin, get_errored_instances_from_context, @@ -14,10 +20,6 @@ from .lib import ( get_instance_staging_dir, ) -from ayon_core.pipeline.colorspace import ( - get_colorspace_settings_from_publish_context, - set_colorspace_data_to_representation -) if typing.TYPE_CHECKING: from ayon_core.pipeline.create import CreateContext, CreatedInstance From 3bbd29dafc262d8984e83e4cf638e6a8b8a033e2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 6 Oct 2024 15:14:22 +0200 Subject: [PATCH 096/546] Show/hide attributes per instance based on status of other toggles --- .../extract_usd_layer_contributions.py | 73 ++++++++++++++++--- 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index acdc5276f7..a67c6ec702 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -458,7 +458,22 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, return new_instance @classmethod - def get_attribute_defs(cls): + def get_attr_defs_for_instance(cls, create_context, instance): + # Filtering of instance, if needed, can be customized + if not cls.instance_matches_plugin_families(instance): + return [] + + # Attributes logic + disabled = False + publish_attributes = instance["publish_attributes"].get( + cls.__name__, {}) + + enabled = publish_attributes.get("contribution_enabled", True) + variant_enabled = enabled and publish_attributes.get( + "contribution_apply_as_variant", True) + + disabled = not enabled + variant_disabled = not variant_enabled return [ UISeparatorDef("usd_container_settings1"), @@ -484,7 +499,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "the contribution itself will be added to the " "department layer." ), - default="usdAsset"), + default="usdAsset", + hidden=disabled), EnumDef("contribution_target_product_init", label="Initialize as", tooltip=( @@ -495,7 +511,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "setting will do nothing." ), items=["asset", "shot"], - default="asset"), + default="asset", + hidden=disabled), # Asset layer, e.g. model.usd, look.usd, rig.usd EnumDef("contribution_layer", @@ -507,7 +524,8 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "the list) will contribute as a stronger opinion." ), items=list(cls.contribution_layers.keys()), - default="model"), + default="model", + hidden=disabled), BoolDef("contribution_apply_as_variant", label="Add as variant", tooltip=( @@ -518,13 +536,16 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "appended to as a sublayer to the department layer " "instead." ), - default=True), + default=True, + hidden=disabled), TextDef("contribution_variant_set_name", label="Variant Set Name", - default="{layer}"), + default="{layer}", + hidden=variant_disabled), TextDef("contribution_variant", label="Variant Name", - default="{variant}"), + default="{variant}", + hidden=variant_disabled), BoolDef("contribution_variant_is_default", label="Set as default variant selection", tooltip=( @@ -535,10 +556,41 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "The behavior is unpredictable if multiple instances " "for the same variant set have this enabled." ), - default=False), + default=False, + hidden=variant_disabled), UISeparatorDef("usd_container_settings3"), ] + @classmethod + def register_create_context_callbacks(cls, create_context): + create_context.add_value_changed_callback(cls.on_values_changed) + + @classmethod + def on_values_changed(cls, event): + """Update instance attribute definitions on attribute changes.""" + + # Update attributes if any of the following plug-in attributes + # change: + keys = ["contribution_enabled", "contribution_apply_as_variant"] + + for instance_change in event["changes"]: + instance = instance_change["instance"] + if not cls.instance_matches_plugin_families(instance): + continue + value_changes = instance_change["changes"] + plugin_attribute_changes = ( + value_changes.get("publish_attributes", {}) + .get(cls.__name__, {})) + + if not any(key in plugin_attribute_changes for key in keys): + continue + + # Update the attribute definitions + new_attrs = cls.get_attr_defs_for_instance( + event["create_context"], instance + ) + instance.set_publish_plugin_attr_defs(cls.__name__, new_attrs) + class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): """ @@ -551,9 +603,8 @@ class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): label = CollectUSDLayerContributions.label + " (Look)" @classmethod - def get_attribute_defs(cls): - defs = super(CollectUSDLayerContributionsHoudiniLook, - cls).get_attribute_defs() + def get_attr_defs_for_instance(cls, create_context, instance): + defs = super().get_attr_defs_for_instance(create_context, instance) # Update default for department layer to look layer_def = next(d for d in defs if d.key == "contribution_layer") From fef043d3e293611988ebab9580c3f09a02c884bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:01:11 +0200 Subject: [PATCH 097/546] use imported classes for typehints --- .../pipeline/publish/publish_plugins.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 9124e8b763..d2c70894cc 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -1,6 +1,7 @@ import inspect from abc import ABCMeta import typing +from typing import Optional import pyblish.api import pyblish.logic @@ -133,7 +134,9 @@ class AYONPyblishPluginMixin: # callback(self) @classmethod - def register_create_context_callbacks(cls, create_context): + def register_create_context_callbacks( + cls, create_context: "CreateContext" + ): """Register callbacks for create context. It is possible to register callbacks listening to changes happened @@ -166,7 +169,7 @@ class AYONPyblishPluginMixin: return [] @classmethod - def get_attr_defs_for_context (cls, create_context): + def get_attr_defs_for_context(cls, create_context: "CreateContext"): """Publish attribute definitions for context. Attributes available for all families in plugin's `families` attribute. @@ -183,7 +186,9 @@ class AYONPyblishPluginMixin: return cls.get_attribute_defs() @classmethod - def instance_matches_plugin_families(cls, instance): + def instance_matches_plugin_families( + cls, instance: Optional["CreatedInstance"] + ): """Check if instance matches families. Args: @@ -207,7 +212,9 @@ class AYONPyblishPluginMixin: return False @classmethod - def get_attr_defs_for_instance(cls, create_context, instance): + def get_attr_defs_for_instance( + cls, create_context: "CreateContext", instance: "CreatedInstance" + ): """Publish attribute definitions for an instance. Attributes available for all families in plugin's `families` attribute. @@ -226,7 +233,9 @@ class AYONPyblishPluginMixin: return cls.get_attribute_defs() @classmethod - def convert_attribute_values(cls, create_context, instance): + def convert_attribute_values( + cls, create_context: "CreateContext", instance: "CreatedInstance" + ): """Convert attribute values for instance. Args: From d9e012c4af33fce61c41a0e67fb3d4439893887a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:12:43 +0200 Subject: [PATCH 098/546] fix key iteration Co-authored-by: Roy Nieterau --- client/ayon_core/tools/publisher/widgets/product_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index f11dc90a5d..c2f1f24d2f 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -917,7 +917,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if instance_id not in self._current_instances_by_id: continue - for key, attr_name in ( + for key in ( "folderPath", "task", "variant", From b2b2ae6cfe369f15289ea09f4512531cf4a52fde Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:19:42 +0200 Subject: [PATCH 099/546] added some type hints --- client/ayon_core/lib/attribute_definitions.py | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 639778b16d..c565a00501 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -6,6 +6,7 @@ import json import copy import warnings from abc import ABCMeta, abstractmethod +from typing import Any, Optional import clique @@ -147,15 +148,15 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def __init__( self, - key, - default, - label=None, - tooltip=None, - is_label_horizontal=None, - visible=None, - enabled=None, - hidden=None, - disabled=None, + key: str, + default: Any, + label: Optional[str] = None, + tooltip: Optional[str] = None, + is_label_horizontal: Optional[bool] = None, + visible: Optional[bool] = None, + enabled: Optional[bool] = None, + hidden: Optional[bool] = None, + disabled: Optional[bool] = None, ): if is_label_horizontal is None: is_label_horizontal = True @@ -167,35 +168,35 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): visible, hidden, "visible", "hidden", True ) - self.key = key - self.label = label - self.tooltip = tooltip - self.default = default - self.is_label_horizontal = is_label_horizontal - self.visible = visible - self.enabled = enabled - self._id = uuid.uuid4().hex + self.key: str = key + self.label: Optional[str] = label + self.tooltip: Optional[str] = tooltip + self.default: Any = default + self.is_label_horizontal: bool = is_label_horizontal + self.visible: bool = visible + self.enabled: bool = enabled + self._id: str = uuid.uuid4().hex self.__init__class__ = AbstractAttrDef @property - def id(self): + def id(self) -> str: return self._id @property - def hidden(self): + def hidden(self) -> bool: return not self.visible @hidden.setter - def hidden(self, value): + def hidden(self, value: bool): self.visible = not value @property - def disabled(self): + def disabled(self) -> bool: return not self.enabled @disabled.setter - def disabled(self, value): + def disabled(self, value: bool): self.enabled = not value def __eq__(self, other): @@ -213,7 +214,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): @property @abstractmethod - def type(self): + def type(self) -> str: """Attribute definition type also used as identifier of class. Returns: @@ -286,7 +287,7 @@ class UILabelDef(UIDef): type = "label" def __init__(self, label, key=None): - super(UILabelDef, self).__init__(label=label, key=key) + super().__init__(label=label, key=key) def __eq__(self, other): if not super(UILabelDef, self).__eq__(other): @@ -309,7 +310,7 @@ class UnknownDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): kwargs["default"] = default - super(UnknownDef, self).__init__(key, **kwargs) + super().__init__(key, **kwargs) def convert_value(self, value): return value @@ -539,7 +540,7 @@ class EnumDef(AbstractAttrDef): return list(self._item_values.intersection(value)) def serialize(self): - data = super(EnumDef, self).serialize() + data = super().serialize() data["items"] = copy.deepcopy(self.items) data["multiselection"] = self.multiselection return data @@ -619,7 +620,7 @@ class BoolDef(AbstractAttrDef): def __init__(self, key, default=None, **kwargs): if default is None: default = False - super(BoolDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) def convert_value(self, value): if isinstance(value, bool): From b8bc6ec2e3c33ec60246dbca7daa8218be9b8ab2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:19:56 +0200 Subject: [PATCH 100/546] simplified comparison --- client/ayon_core/lib/attribute_definitions.py | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index c565a00501..d5b9239809 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -199,18 +199,31 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def disabled(self, value: bool): self.enabled = not value - def __eq__(self, other): - if not isinstance(other, self.__class__): + def __eq__(self, other: Any) -> bool: + return self.compare_to_def(other) + + def __ne__(self, other: Any) -> bool: + return not self.compare_to_def(other) + + def compare_to_def( + self, + other: Any, + ignore_default: Optional[bool] = False, + ignore_enabled: Optional[bool] = False, + ignore_visible: Optional[bool] = False, + ) -> bool: + if not isinstance(other, self.__class__) or self.key != other.key: + return False + if not self._custom_def_compare(other): return False return ( - self.key == other.key - and self.default == other.default - and self.visible == other.visible - and self.enabled == other.enabled + (ignore_default or self.default == other.default) + and (ignore_visible or self.visible == other.visible) + and (ignore_enabled or self.enabled == other.enabled) ) - def __ne__(self, other): - return not self.__eq__(other) + def _custom_def_compare(self, other: "AbstractAttrDef") -> bool: + return True @property @abstractmethod @@ -289,9 +302,7 @@ class UILabelDef(UIDef): def __init__(self, label, key=None): super().__init__(label=label, key=key) - def __eq__(self, other): - if not super(UILabelDef, self).__eq__(other): - return False + def _custom_def_compare(self, other: "UILabelDef") -> bool: return self.label == other.label @@ -387,10 +398,7 @@ class NumberDef(AbstractAttrDef): self.maximum = maximum self.decimals = 0 if decimals is None else decimals - def __eq__(self, other): - if not super(NumberDef, self).__eq__(other): - return False - + def _custom_def_compare(self, other: "NumberDef") -> bool: return ( self.decimals == other.decimals and self.maximum == other.maximum @@ -457,10 +465,8 @@ class TextDef(AbstractAttrDef): self.placeholder = placeholder self.regex = regex - def __eq__(self, other): - if not super(TextDef, self).__eq__(other): - return False + def _custom_def_compare(self, other: "TextDef") -> bool: return ( self.multiline == other.multiline and self.regex == other.regex @@ -514,16 +520,13 @@ class EnumDef(AbstractAttrDef): elif default not in item_values: default = next(iter(item_values), None) - super(EnumDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) self.items = items self._item_values = item_values_set self.multiselection = multiselection - def __eq__(self, other): - if not super(EnumDef, self).__eq__(other): - return False - + def _custom_def_compare(self, other: "EnumDef") -> bool: return ( self.items == other.items and self.multiselection == other.multiselection From 116061aefbfa7e78c5128998ec98242f8d933038 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:25:19 +0200 Subject: [PATCH 101/546] added option to skip def specific comparison --- client/ayon_core/lib/attribute_definitions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index d5b9239809..99f9e9988f 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -211,10 +211,11 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): ignore_default: Optional[bool] = False, ignore_enabled: Optional[bool] = False, ignore_visible: Optional[bool] = False, + ignore_def_type_compare: Optional[bool] = False, ) -> bool: if not isinstance(other, self.__class__) or self.key != other.key: return False - if not self._custom_def_compare(other): + if not ignore_def_type_compare and not self._def_type_compare(other): return False return ( (ignore_default or self.default == other.default) @@ -222,7 +223,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): and (ignore_enabled or self.enabled == other.enabled) ) - def _custom_def_compare(self, other: "AbstractAttrDef") -> bool: + def _def_type_compare(self, other: "AbstractAttrDef") -> bool: return True @property @@ -398,7 +399,7 @@ class NumberDef(AbstractAttrDef): self.maximum = maximum self.decimals = 0 if decimals is None else decimals - def _custom_def_compare(self, other: "NumberDef") -> bool: + def _def_type_compare(self, other: "NumberDef") -> bool: return ( self.decimals == other.decimals and self.maximum == other.maximum @@ -465,8 +466,7 @@ class TextDef(AbstractAttrDef): self.placeholder = placeholder self.regex = regex - - def _custom_def_compare(self, other: "TextDef") -> bool: + def _def_type_compare(self, other: "TextDef") -> bool: return ( self.multiline == other.multiline and self.regex == other.regex @@ -526,7 +526,7 @@ class EnumDef(AbstractAttrDef): self._item_values = item_values_set self.multiselection = multiselection - def _custom_def_compare(self, other: "EnumDef") -> bool: + def _def_type_compare(self, other: "EnumDef") -> bool: return ( self.items == other.items and self.multiselection == other.multiselection From 0c801ba892a011cc2a2d0278886a2d1f0d609cf1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:25:32 +0200 Subject: [PATCH 102/546] fix ui label --- client/ayon_core/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 99f9e9988f..01f5606f17 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -303,7 +303,7 @@ class UILabelDef(UIDef): def __init__(self, label, key=None): super().__init__(label=label, key=key) - def _custom_def_compare(self, other: "UILabelDef") -> bool: + def _def_type_compare(self, other: "UILabelDef") -> bool: return self.label == other.label From 88e4a159ca79424c156afd832d2f924985b2e46d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:25:50 +0200 Subject: [PATCH 103/546] fix clone --- client/ayon_core/lib/attribute_definitions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 01f5606f17..dd467797f1 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -183,6 +183,11 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self) -> str: return self._id + def clone(self): + data = self.serialize() + data.pop("type") + return self.deserialize(data) + @property def hidden(self) -> bool: return not self.visible @@ -275,6 +280,9 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Data can be received using 'serialize' method. """ + if "type" in data: + data = dict(data) + data.pop("type") return cls(**data) From 5ca53978fcd8165fc24832e8da5b5edd70b3b5bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:26:07 +0200 Subject: [PATCH 104/546] added missing data to serialization o textdef --- client/ayon_core/lib/attribute_definitions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index dd467797f1..8ce270c218 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -488,6 +488,8 @@ class TextDef(AbstractAttrDef): def serialize(self): data = super(TextDef, self).serialize() data["regex"] = self.regex.pattern + data["multiline"] = self.multiline + data["placeholder"] = self.placeholder return data From 87a909ae40b497b03db01921aae2f695c14def86 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:26:17 +0200 Subject: [PATCH 105/546] use py3 super --- client/ayon_core/lib/attribute_definitions.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 8ce270c218..6a0a10c349 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -295,7 +295,7 @@ class UIDef(AbstractAttrDef): is_value_def = False def __init__(self, key=None, default=None, *args, **kwargs): - super(UIDef, self).__init__(key, default, *args, **kwargs) + super().__init__(key, default, *args, **kwargs) def convert_value(self, value): return value @@ -401,7 +401,7 @@ class NumberDef(AbstractAttrDef): elif default > maximum: default = maximum - super(NumberDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) self.minimum = minimum self.maximum = maximum @@ -457,7 +457,7 @@ class TextDef(AbstractAttrDef): if default is None: default = "" - super(TextDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) if multiline is None: multiline = False @@ -486,7 +486,7 @@ class TextDef(AbstractAttrDef): return self.default def serialize(self): - data = super(TextDef, self).serialize() + data = super().serialize() data["regex"] = self.regex.pattern data["multiline"] = self.multiline data["placeholder"] = self.placeholder @@ -931,10 +931,10 @@ class FileDef(AbstractAttrDef): self.extensions = set(extensions) self.allow_sequences = allow_sequences self.extensions_label = extensions_label - super(FileDef, self).__init__(key, default=default, **kwargs) + super().__init__(key, default=default, **kwargs) def __eq__(self, other): - if not super(FileDef, self).__eq__(other): + if not super().__eq__(other): return False return ( From a2170a76fe055e58d906fb75d056dc344eeb1160 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:26:41 +0200 Subject: [PATCH 106/546] initial idea of merging attributes --- .../tools/publisher/models/create.py | 79 ++++++++++++++++++- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 4b27081db2..577340d053 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -6,6 +6,7 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, AbstractAttrDef, + EnumDef, ) from ayon_core.lib.profiles_filtering import filter_profiles from ayon_core.lib.attribute_definitions import UIDef @@ -296,6 +297,71 @@ class InstanceItem: ) +def _merge_attr_defs( + attr_def_src: AbstractAttrDef, attr_def_new: AbstractAttrDef +) -> Optional[AbstractAttrDef]: + if not attr_def_src.enabled and attr_def_new.enabled: + attr_def_src.enabled = True + if not attr_def_src.visible and attr_def_new.visible: + attr_def_src.visible = True + + if not isinstance(attr_def_src, EnumDef): + return None + if attr_def_src.items == attr_def_new.items: + return None + + src_item_values = { + item["value"] + for item in attr_def_src + } + for item in attr_def_new.items: + if item["value"] not in src_item_values: + attr_def_src.items.append(item) + + +def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): + if not attr_defs: + return [] + if len(attr_defs) == 1: + return attr_defs[0] + + # Pop first and create clone of attribute definitions + defs_union: List[AbstractAttrDef] = [ + attr_def.clone() + for attr_def in attr_defs.pop(0) + ] + for instance_attr_defs in attr_defs: + idx = 0 + for attr_idx, attr_def in enumerate(instance_attr_defs): + is_enum = isinstance(attr_def, EnumDef) + match_idx = None + match_attr = None + for union_idx, union_def in enumerate(defs_union): + if ( + attr_def.compare_to_def( + union_def, + ignore_default=True, + ignore_enabled=True, + ignore_visible=True, + ignore_def_type_compare=is_enum + ) + ): + match_idx = union_idx + match_attr = union_def + break + + if match_attr is not None: + new_attr_def = _merge_attr_defs(match_attr, attr_def) + if new_attr_def is not None: + defs_union[match_idx] = new_attr_def + idx = match_idx + 1 + continue + + defs_union.insert(idx, attr_def.clone()) + idx += 1 + return defs_union + + class CreateModel: def __init__(self, controller: AbstractPublisherBackend): self._log = None @@ -729,9 +795,10 @@ class CreateModel: attr_defs = attr_val.attr_defs if not attr_defs: continue - - if plugin_name not in all_defs_by_plugin_name: - all_defs_by_plugin_name[plugin_name] = attr_val.attr_defs + plugin_attr_defs = all_defs_by_plugin_name.setdefault( + plugin_name, [] + ) + plugin_attr_defs.append(attr_defs) plugin_values = all_plugin_values.setdefault(plugin_name, {}) @@ -744,6 +811,10 @@ class CreateModel: value = attr_val[attr_def.key] attr_values.append((item_id, value)) + attr_defs_by_plugin_name = {} + for plugin_name, attr_defs in all_defs_by_plugin_name.items(): + attr_defs_by_plugin_name[plugin_name] = merge_attr_defs(attr_defs) + output = [] for plugin in self._create_context.plugins_with_defs: plugin_name = plugin.__name__ @@ -751,7 +822,7 @@ class CreateModel: continue output.append(( plugin_name, - all_defs_by_plugin_name[plugin_name], + attr_defs_by_plugin_name[plugin_name], all_plugin_values )) return output From b0001f9a5886a4e658bc47a7fd828bcd50f0eee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:32:47 +0200 Subject: [PATCH 107/546] fix values loop --- client/ayon_core/tools/publisher/models/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 577340d053..536e1475ea 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -312,7 +312,7 @@ def _merge_attr_defs( src_item_values = { item["value"] - for item in attr_def_src + for item in attr_def_src.items } for item in attr_def_new.items: if item["value"] not in src_item_values: From ed36077292d28ea3fee027f1545b4fd3289a2360 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:17:16 +0200 Subject: [PATCH 108/546] added 'is_value_valid' implementation for attribute definitions --- client/ayon_core/lib/attribute_definitions.py | 117 +++++++++++++++--- 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 6a0a10c349..4877a45118 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -228,8 +228,21 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): and (ignore_enabled or self.enabled == other.enabled) ) - def _def_type_compare(self, other: "AbstractAttrDef") -> bool: - return True + @abstractmethod + def is_value_valid(self, value: Any) -> bool: + """Check if value is valid. + + This should return False if value is not valid based + on definition type. + + Args: + value (Any): Value to validate based on definition type. + + Returns: + bool: True if value is valid. + + """ + pass @property @abstractmethod @@ -286,6 +299,9 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return cls(**data) + def _def_type_compare(self, other: "AbstractAttrDef") -> bool: + return True + # ----------------------------------------- # UI attribute definitions won't hold value @@ -297,6 +313,9 @@ class UIDef(AbstractAttrDef): def __init__(self, key=None, default=None, *args, **kwargs): super().__init__(key, default, *args, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return True + def convert_value(self, value): return value @@ -332,6 +351,9 @@ class UnknownDef(AbstractAttrDef): kwargs["default"] = default super().__init__(key, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return True + def convert_value(self, value): return value @@ -352,6 +374,9 @@ class HiddenDef(AbstractAttrDef): kwargs["visible"] = False super().__init__(key, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return True + def convert_value(self, value): return value @@ -407,12 +432,15 @@ class NumberDef(AbstractAttrDef): self.maximum = maximum self.decimals = 0 if decimals is None else decimals - def _def_type_compare(self, other: "NumberDef") -> bool: - return ( - self.decimals == other.decimals - and self.maximum == other.maximum - and self.maximum == other.maximum - ) + def is_value_valid(self, value: Any) -> bool: + if self.decimals == 0: + if not isinstance(value, int): + return False + elif not isinstance(value, float): + return False + if self.minimum > value > self.maximum: + return False + return True def convert_value(self, value): if isinstance(value, str): @@ -428,6 +456,13 @@ class NumberDef(AbstractAttrDef): return int(value) return round(float(value), self.decimals) + def _def_type_compare(self, other: "NumberDef") -> bool: + return ( + self.decimals == other.decimals + and self.maximum == other.maximum + and self.maximum == other.maximum + ) + class TextDef(AbstractAttrDef): """Text definition. @@ -474,11 +509,12 @@ class TextDef(AbstractAttrDef): self.placeholder = placeholder self.regex = regex - def _def_type_compare(self, other: "TextDef") -> bool: - return ( - self.multiline == other.multiline - and self.regex == other.regex - ) + def is_value_valid(self, value: Any) -> bool: + if not isinstance(value, str): + return False + if self.regex and not self.regex.match(value): + return False + return True def convert_value(self, value): if isinstance(value, str): @@ -492,6 +528,12 @@ class TextDef(AbstractAttrDef): data["placeholder"] = self.placeholder return data + def _def_type_compare(self, other: "TextDef") -> bool: + return ( + self.multiline == other.multiline + and self.regex == other.regex + ) + class EnumDef(AbstractAttrDef): """Enumeration of items. @@ -536,12 +578,6 @@ class EnumDef(AbstractAttrDef): self._item_values = item_values_set self.multiselection = multiselection - def _def_type_compare(self, other: "EnumDef") -> bool: - return ( - self.items == other.items - and self.multiselection == other.multiselection - ) - def convert_value(self, value): if not self.multiselection: if value in self._item_values: @@ -552,6 +588,17 @@ class EnumDef(AbstractAttrDef): return copy.deepcopy(self.default) return list(self._item_values.intersection(value)) + def is_value_valid(self, value: Any) -> bool: + """Check if item is available in possible values.""" + if isinstance(value, list): + if not self.multiselection: + return False + return all(value in self._item_values for value in value) + + if self.multiselection: + return False + return value in self._item_values + def serialize(self): data = super().serialize() data["items"] = copy.deepcopy(self.items) @@ -620,6 +667,12 @@ class EnumDef(AbstractAttrDef): return output + def _def_type_compare(self, other: "EnumDef") -> bool: + return ( + self.items == other.items + and self.multiselection == other.multiselection + ) + class BoolDef(AbstractAttrDef): """Boolean representation. @@ -635,6 +688,9 @@ class BoolDef(AbstractAttrDef): default = False super().__init__(key, default=default, **kwargs) + def is_value_valid(self, value: Any) -> bool: + return isinstance(value, bool) + def convert_value(self, value): if isinstance(value, bool): return value @@ -944,6 +1000,29 @@ class FileDef(AbstractAttrDef): and self.allow_sequences == other.allow_sequences ) + def is_value_valid(self, value: Any) -> bool: + if self.single_item: + if not isinstance(value, dict): + return False + try: + FileDefItem.from_dict(value) + return True + except (ValueError, KeyError): + return False + + if not isinstance(value, list): + return False + + for item in value: + if not isinstance(item, dict): + return False + + try: + FileDefItem.from_dict(item) + except (ValueError, KeyError): + return False + return True + def convert_value(self, value): if isinstance(value, (str, dict)): value = [value] From a9e58c40477d374d4ba5935f91757f8b82f71b46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:17:27 +0200 Subject: [PATCH 109/546] added method to get attribute definition --- client/ayon_core/pipeline/create/structures.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 8594d82848..bcc9a87c49 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -148,6 +148,9 @@ class AttributeValues: for key in self._attr_defs_by_key.keys(): yield key, self._data.get(key) + def get_attr_def(self, key, default=None): + return self._attr_defs_by_key.get(key, default) + def update(self, value): changes = {} for _key, _value in dict(value).items(): From 87329306d4a791b90e540ec49054ce187aa6d2df Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:17:45 +0200 Subject: [PATCH 110/546] don't set all values unless they are valid for the instance --- .../tools/publisher/models/create.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 536e1475ea..493fcc3b01 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -333,10 +333,18 @@ def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): for instance_attr_defs in attr_defs: idx = 0 for attr_idx, attr_def in enumerate(instance_attr_defs): + # QUESTION should we merge NumberDef too? Use lowest min and + # biggest max... is_enum = isinstance(attr_def, EnumDef) match_idx = None match_attr = None for union_idx, union_def in enumerate(defs_union): + if is_enum and ( + not isinstance(union_def, EnumDef) + or union_def.multiselection != attr_def.multiselection + ): + continue + if ( attr_def.compare_to_def( union_def, @@ -759,6 +767,18 @@ class CreateModel: else: instance = self._get_instance_by_id(instance_id) plugin_val = instance.publish_attributes[plugin_name] + attr_def = plugin_val.get_attr_def(key) + # Ignore if attribute is not available or enabled/visible + # on the instance, or the value is not valid for definition + if ( + attr_def is None + or not attr_def.is_value_def + or not attr_def.visible + or not attr_def.enabled + or not attr_def.is_value_valid(value) + ): + continue + plugin_val[key] = value def get_publish_attribute_definitions( From c3aff4deb2751796178ee94940abf437658bec13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 9 Oct 2024 15:21:16 +0200 Subject: [PATCH 111/546] implemented similar logic to create attributes --- client/ayon_core/tools/publisher/models/create.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 493fcc3b01..f2a7901e42 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -717,8 +717,16 @@ class CreateModel: for instance_id in instance_ids: instance = self._get_instance_by_id(instance_id) creator_attributes = instance["creator_attributes"] - if key in creator_attributes: - creator_attributes[key] = value + attr_def = creator_attributes.get_attr_def(key) + if ( + attr_def is None + or not attr_def.is_value_def + or not attr_def.visible + or not attr_def.enabled + or not attr_def.is_value_valid(value) + ): + continue + creator_attributes[key] = value def get_creator_attribute_definitions( self, instance_ids: List[str] From e2eb8260deff0b5b108f4a613f97d426a422b6e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:41:07 +0200 Subject: [PATCH 112/546] fix validation of context in main window --- client/ayon_core/tools/publisher/control.py | 22 ++++++++++++++++++- .../tools/publisher/models/create.py | 22 ++++++++++++++++++- client/ayon_core/tools/publisher/window.py | 19 ++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 43b491a20f..51eaefe0e0 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -35,7 +35,27 @@ class PublisherController( Known topics: "show.detailed.help" - Detailed help requested (UI related). "show.card.message" - Show card message request (UI related). - "instances.refresh.finished" - Instances are refreshed. + # --- Create model --- + "create.model.reset" - Reset of create model. + "instances.create.failed" - Creation failed. + "convertors.convert.failed" - Convertor failed. + "instances.save.failed" - Save failed. + "instance.thumbnail.changed" - Thumbnail changed. + "instances.collection.failed" - Collection of instances failed. + "convertors.find.failed" - Convertor find failed. + "instances.create.failed" - Create instances failed. + "instances.remove.failed" - Remove instances failed. + "create.context.added.instance" - Create instance added to context. + "create.context.value.changed" - Create instance or context value + changed. + "create.context.pre.create.attrs.changed" - Pre create attributes + changed. + "create.context.create.attrs.changed" - Create attributes changed. + "create.context.publish.attrs.changed" - Publish attributes changed. + "create.context.removed.instance" - Instance removed from context. + "create.model.instances.context.changed" - Instances changed context. + like folder, task or variant. + # --- Publish model --- "plugins.refresh.finished" - Plugins refreshed. "publish.reset.finished" - Reset finished. "controller.reset.started" - Controller reset started. diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index f2a7901e42..a08a3c6863 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -371,6 +371,8 @@ def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): class CreateModel: + _CONTEXT_KEYS = {"folderPath", "task", "variant", "productName"} + def __init__(self, controller: AbstractPublisherBackend): self._log = None self._controller: AbstractPublisherBackend = controller @@ -527,6 +529,12 @@ class CreateModel: instance = self._get_instance_by_id(instance_id) for key, value in changes.items(): instance[key] = value + self._emit_event( + "create.model.instances.context.changed", + { + "instance_ids": list(changes_by_instance_id.keys()) + } + ) def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id @@ -1032,16 +1040,28 @@ class CreateModel: return instance_changes = {} + context_changed_ids = set() for item in event.data["changes"]: instance_id = None if item["instance"]: instance_id = item["instance"].id - instance_changes[instance_id] = item["changes"] + changes = item["changes"] + instance_changes[instance_id] = changes + if instance_id is None: + continue + + if self._CONTEXT_KEYS.intersection(set(changes)): + context_changed_ids.add(instance_id) self._emit_event( "create.context.value.changed", {"instance_changes": instance_changes}, ) + if context_changed_ids: + self._emit_event( + "create.model.instances.context.changed", + {"instance_ids": list(context_changed_ids)}, + ) def _cc_pre_create_attr_changed(self, event): identifiers = event["identifiers"] diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index e4da71b3d6..1d16159ffa 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -281,7 +281,19 @@ class PublisherWindow(QtWidgets.QDialog): ) controller.register_event_callback( - "instances.refresh.finished", self._on_instances_refresh + "create.model.reset", self._on_create_model_reset + ) + controller.register_event_callback( + "create.context.added.instance", + self._event_callback_validate_instances + ) + controller.register_event_callback( + "create.context.removed.instance", + self._event_callback_validate_instances + ) + controller.register_event_callback( + "create.model.instances.context.changed", + self._event_callback_validate_instances ) controller.register_event_callback( "publish.reset.finished", self._on_publish_reset @@ -936,13 +948,16 @@ class PublisherWindow(QtWidgets.QDialog): self._set_footer_enabled(bool(all_valid)) - def _on_instances_refresh(self): + def _on_create_model_reset(self): self._validate_create_instances() context_title = self._controller.get_context_title() self.set_context_label(context_title) self._update_publish_details_widget() + def _event_callback_validate_instances(self, _event): + self._validate_create_instances() + def _set_comment_input_visiblity(self, visible): self._comment_input.setVisible(visible) self._footer_spacer.setVisible(not visible) From 36541c5aae0023f479f343521fc25ee0e7010134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 11 Oct 2024 17:45:41 +0200 Subject: [PATCH 113/546] :art: add unreal to hosts unreal can do local rendering/publishing and without it, it is missing thumbnail. --- client/ayon_core/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 4ffabf6028..37bbac8898 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -36,7 +36,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "traypublisher", "substancepainter", "nuke", - "aftereffects" + "aftereffects", + "unreal" ] enabled = False From 047cc8e0cf90d367f683739642ea25c131af2fe2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:01:20 +0200 Subject: [PATCH 114/546] fix issues in list view widgets --- .../tools/publisher/widgets/list_view_widgets.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index 14814a4aa6..a6a2f08752 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -110,7 +110,7 @@ class ListItemDelegate(QtWidgets.QStyledItemDelegate): class InstanceListItemWidget(QtWidgets.QWidget): """Widget with instance info drawn over delegate paint. - This is required to be able use custom checkbox on custom place. + This is required to be able to use custom checkbox on custom place. """ active_changed = QtCore.Signal(str, bool) double_clicked = QtCore.Signal() @@ -245,8 +245,8 @@ class ListContextWidget(QtWidgets.QFrame): class InstanceListGroupWidget(QtWidgets.QFrame): """Widget representing group of instances. - Has collapse/expand indicator, label of group and checkbox modifying all of - it's children. + Has collapse/expand indicator, label of group and checkbox modifying all + of its children. """ expand_changed = QtCore.Signal(str, bool) toggle_requested = QtCore.Signal(str, int) @@ -392,7 +392,7 @@ class InstanceTreeView(QtWidgets.QTreeView): def _mouse_press(self, event): """Store index of pressed group. - This is to be able change state of group and process mouse + This is to be able to change state of group and process mouse "double click" as 2x "single click". """ if event.button() != QtCore.Qt.LeftButton: @@ -588,7 +588,7 @@ class InstanceListView(AbstractInstanceView): # Prepare instances by their groups instances_by_group_name = collections.defaultdict(list) group_names = set() - for instance in self._controller.get_instances(): + for instance in self._controller.get_instance_items(): group_label = instance.group_label group_names.add(group_label) instances_by_group_name[group_label].append(instance) @@ -612,7 +612,7 @@ class InstanceListView(AbstractInstanceView): # Mapping of existing instances under group item existing_mapping = {} - # Get group index to be able get children indexes + # Get group index to be able to get children indexes group_index = self._instance_model.index( group_item.row(), group_item.column() ) From 5f380d244b9f86c9d99b2af444db8da3f6f2c921 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:29:55 +0200 Subject: [PATCH 115/546] consider active as context change --- client/ayon_core/tools/publisher/models/create.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index a08a3c6863..a68b5e2879 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -371,7 +371,13 @@ def merge_attr_defs(attr_defs: List[List[AbstractAttrDef]]): class CreateModel: - _CONTEXT_KEYS = {"folderPath", "task", "variant", "productName"} + _CONTEXT_KEYS = { + "active", + "folderPath", + "task", + "variant", + "productName", + } def __init__(self, controller: AbstractPublisherBackend): self._log = None From cc3aa6936eccb21d5dc3e5428a54a5bd92853680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:30:06 +0200 Subject: [PATCH 116/546] remove unnecessary callbacks --- client/ayon_core/tools/publisher/window.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index 1d16159ffa..a912495d4e 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -253,12 +253,6 @@ class PublisherWindow(QtWidgets.QDialog): help_btn.clicked.connect(self._on_help_click) tabs_widget.tab_changed.connect(self._on_tab_change) - overview_widget.active_changed.connect( - self._on_context_or_active_change - ) - overview_widget.instance_context_changed.connect( - self._on_context_or_active_change - ) overview_widget.create_requested.connect( self._on_create_request ) From a21341ad1588f53ab74789cd84f4833580045238 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:43:38 +0200 Subject: [PATCH 117/546] views are propagating context changes --- .../publisher/widgets/card_view_widgets.py | 36 ++++++++++++------- .../publisher/widgets/list_view_widgets.py | 19 +++++++--- .../publisher/widgets/overview_widget.py | 14 ++++---- .../publisher/widgets/product_context.py | 3 -- .../tools/publisher/widgets/product_info.py | 12 +++---- 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 6ef34b86f8..67793bb50e 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -22,6 +22,7 @@ Only one item can be selected at a time. import re import collections +from typing import Dict from qtpy import QtWidgets, QtCore @@ -217,11 +218,18 @@ class InstanceGroupWidget(BaseGroupWidget): def update_icons(self, group_icons): self._group_icons = group_icons - def update_instance_values(self, context_info_by_id): + def update_instance_values( + self, context_info_by_id, instance_items_by_id, instance_ids + ): """Trigger update on instance widgets.""" for instance_id, widget in self._widgets_by_id.items(): - widget.update_instance_values(context_info_by_id[instance_id]) + if instance_ids is not None and instance_id not in instance_ids: + continue + widget.update_instance( + instance_items_by_id[instance_id], + context_info_by_id[instance_id] + ) def update_instances(self, instances, context_info_by_id): """Update instances for the group. @@ -391,9 +399,6 @@ class ConvertorItemCardWidget(CardWidget): self._icon_widget = icon_widget self._label_widget = label_widget - def update_instance_values(self, context_info): - pass - class InstanceCardWidget(CardWidget): """Card widget representing instance.""" @@ -461,7 +466,7 @@ class InstanceCardWidget(CardWidget): self._active_checkbox = active_checkbox self._expand_btn = expand_btn - self.update_instance_values(context_info) + self._update_instance_context(context_info) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -486,7 +491,7 @@ class InstanceCardWidget(CardWidget): def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance - self.update_instance_values(context_info) + self._update_instance_context(context_info) def _validate_context(self, context_info): valid = context_info.is_valid @@ -522,7 +527,7 @@ class InstanceCardWidget(CardWidget): QtCore.Qt.NoTextInteraction ) - def update_instance_values(self, context_info): + def _update_instance_context(self, context_info): """Update instance data""" self._update_product_name() self.set_active(self.instance.is_active) @@ -596,7 +601,7 @@ class InstanceCardView(AbstractInstanceView): self._context_widget = None self._convertor_items_group = None self._active_toggle_enabled = True - self._widgets_by_group = {} + self._widgets_by_group: Dict[str, InstanceGroupWidget] = {} self._ordered_groups = [] self._explicitly_selected_instance_ids = [] @@ -702,7 +707,7 @@ class InstanceCardView(AbstractInstanceView): # Prepare instances by group and identifiers by group instances_by_group = collections.defaultdict(list) identifiers_by_group = collections.defaultdict(set) - for instance in self._controller.get_instances(): + for instance in self._controller.get_instance_items(): group_name = instance.group_label instances_by_group[group_name].append(instance) identifiers_by_group[group_name].add( @@ -817,11 +822,18 @@ class InstanceCardView(AbstractInstanceView): self._convertor_items_group.update_items(convertor_items) - def refresh_instance_states(self): + def refresh_instance_states(self, instance_ids=None): """Trigger update of instances on group widgets.""" + if instance_ids is not None: + instance_ids = set(instance_ids) context_info_by_id = self._controller.get_instances_context_info() + instance_items_by_id = self._controller.get_instance_items_by_id( + instance_ids + ) for widget in self._widgets_by_group.values(): - widget.update_instance_values(context_info_by_id) + widget.update_instance_values( + context_info_by_id, instance_items_by_id, instance_ids + ) def _on_active_changed(self, group_name, instance_id, value): group_widget = self._widgets_by_group[group_name] diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index a6a2f08752..a8144e71f4 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -191,9 +191,9 @@ class InstanceListItemWidget(QtWidgets.QWidget): def update_instance(self, instance, context_info): """Update instance object.""" self.instance = instance - self.update_instance_values(context_info) + self._update_instance_values(context_info) - def update_instance_values(self, context_info): + def _update_instance_values(self, context_info): """Update instance data propagated to widgets.""" # Check product name label = self.instance.label @@ -873,12 +873,21 @@ class InstanceListView(AbstractInstanceView): widget = self._group_widgets.pop(group_name) widget.deleteLater() - def refresh_instance_states(self): + def refresh_instance_states(self, instance_ids=None): """Trigger update of all instances.""" + if instance_ids is not None: + instance_ids = set(instance_ids) context_info_by_id = self._controller.get_instances_context_info() + instance_items_by_id = self._controller.get_instance_items_by_id( + instance_ids + ) for instance_id, widget in self._widgets_by_id.items(): - context_info = context_info_by_id[instance_id] - widget.update_instance_values(context_info) + if instance_ids is not None and instance_id not in instance_ids: + continue + widget.update_instance( + instance_items_by_id[instance_id], + context_info_by_id[instance_id], + ) def _on_active_changed(self, changed_instance_id, new_value): selected_instance_ids, _, _ = self.get_selected_items() diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index beefa1ca98..5e8b803fc3 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -16,7 +16,6 @@ from .product_info import ProductInfoWidget class OverviewWidget(QtWidgets.QFrame): active_changed = QtCore.Signal() - instance_context_changed = QtCore.Signal() create_requested = QtCore.Signal() convert_requested = QtCore.Signal() publish_tab_requested = QtCore.Signal() @@ -134,9 +133,6 @@ class OverviewWidget(QtWidgets.QFrame): self._on_active_changed ) # Instance context has changed - product_attributes_widget.instance_context_changed.connect( - self._on_instance_context_change - ) product_attributes_widget.convert_requested.connect( self._on_convert_requested ) @@ -163,6 +159,10 @@ class OverviewWidget(QtWidgets.QFrame): "create.context.removed.instance", self._on_instances_removed ) + controller.register_event_callback( + "create.model.instances.context.changed", + self._on_instance_context_change + ) self._product_content_widget = product_content_widget self._product_content_layout = product_content_layout @@ -362,7 +362,7 @@ class OverviewWidget(QtWidgets.QFrame): self._current_state == "publish" ) - def _on_instance_context_change(self): + def _on_instance_context_change(self, event): current_idx = self._product_views_layout.currentIndex() for idx in range(self._product_views_layout.count()): if idx == current_idx: @@ -372,9 +372,7 @@ class OverviewWidget(QtWidgets.QFrame): widget.set_refreshed(False) current_widget = self._product_views_layout.widget(current_idx) - current_widget.refresh_instance_states() - - self.instance_context_changed.emit() + current_widget.refresh_instance_states(event["instance_ids"]) def _on_convert_requested(self): self.convert_requested.emit() diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index c2f1f24d2f..04c9ca7e56 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -621,7 +621,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): product name: [ immutable ] [Submit] [Cancel] """ - instance_context_changed = QtCore.Signal() multiselection_text = "< Multiselection >" unknown_value = "N/A" @@ -775,7 +774,6 @@ class GlobalAttrsWidget(QtWidgets.QWidget): self._controller.set_instances_context_info(changes_by_id) self._refresh_items() - self.instance_context_changed.emit() def _on_cancel(self): """Cancel changes and set back to their irigin value.""" @@ -933,4 +931,3 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if changed: self._refresh_items() self._refresh_content() - self.instance_context_changed.emit() diff --git a/client/ayon_core/tools/publisher/widgets/product_info.py b/client/ayon_core/tools/publisher/widgets/product_info.py index 9a7700d73d..27b7aacf38 100644 --- a/client/ayon_core/tools/publisher/widgets/product_info.py +++ b/client/ayon_core/tools/publisher/widgets/product_info.py @@ -26,7 +26,6 @@ class ProductInfoWidget(QtWidgets.QWidget): │ │ attributes │ └───────────────────────────────┘ """ - instance_context_changed = QtCore.Signal() convert_requested = QtCore.Signal() def __init__( @@ -123,13 +122,14 @@ class ProductInfoWidget(QtWidgets.QWidget): self._context_selected = False self._all_instances_valid = True - global_attrs_widget.instance_context_changed.connect( - self._on_instance_context_changed - ) convert_btn.clicked.connect(self._on_convert_click) thumbnail_widget.thumbnail_created.connect(self._on_thumbnail_create) thumbnail_widget.thumbnail_cleared.connect(self._on_thumbnail_clear) + controller.register_event_callback( + "create.model.instances.context.changed", + self._on_instance_context_change + ) controller.register_event_callback( "instance.thumbnail.changed", self._on_thumbnail_changed @@ -196,7 +196,7 @@ class ProductInfoWidget(QtWidgets.QWidget): self._update_thumbnails() - def _on_instance_context_changed(self): + def _on_instance_context_change(self): instance_ids = { instance.id for instance in self._current_instances @@ -214,8 +214,6 @@ class ProductInfoWidget(QtWidgets.QWidget): self.creator_attrs_widget.set_instances_valid(all_valid) self.publish_attrs_widget.set_instances_valid(all_valid) - self.instance_context_changed.emit() - def _on_convert_click(self): self.convert_requested.emit() From 22c3142894ac83af93e72c990f79313c8163060b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:05:59 +0200 Subject: [PATCH 118/546] added helper method to change active state --- client/ayon_core/tools/publisher/abstract.py | 7 ++++ client/ayon_core/tools/publisher/control.py | 3 ++ .../tools/publisher/models/create.py | 35 +++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 3a968eee28..4787e8a21b 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -3,6 +3,7 @@ from typing import ( Optional, Dict, List, + Set, Tuple, Any, Callable, @@ -353,6 +354,12 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def set_instances_active_state( + self, active_state_by_id: Dict[str, bool] + ): + pass + @abstractmethod def get_existing_product_names(self, folder_path: str) -> List[str]: pass diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 51eaefe0e0..347755d557 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -220,6 +220,9 @@ class PublisherController( changes_by_instance_id ) + def set_instances_active_state(self, active_state_by_id): + self._create_model.set_instances_active_state(active_state_by_id) + def get_convertor_items(self): return self._create_model.get_convertor_items() diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index a68b5e2879..2aa7b169a0 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -1,6 +1,16 @@ import logging import re -from typing import Union, List, Dict, Tuple, Any, Optional, Iterable, Pattern +from typing import ( + Union, + List, + Dict, + Set, + Tuple, + Any, + Optional, + Iterable, + Pattern, +) from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, @@ -542,6 +552,21 @@ class CreateModel: } ) + def set_instances_active_state( + self, active_state_by_id: Dict[str, bool] + ): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id, active in active_state_by_id.items(): + instance = self._create_context.get_instance_by_id(instance_id) + instance["active"] = active + + self._emit_event( + "create.model.instances.context.changed", + { + "instance_ids": set(active_state_by_id.keys()) + } + ) + def get_convertor_items(self) -> Dict[str, ConvertorItem]: return self._create_context.convertor_items_by_id @@ -896,8 +921,12 @@ class CreateModel: } ) - def _emit_event(self, topic: str, data: Optional[Dict[str, Any]] = None): - self._controller.emit_event(topic, data) + def _emit_event( + self, + topic: str, + data: Optional[Dict[str, Any]] = None + ): + self._controller.emit_event(topic, data, CREATE_EVENT_SOURCE) def _get_current_project_settings(self) -> Dict[str, Any]: """Current project settings. From c3641b380dd0dd6bac41b6e983faf0d551f1f41b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:11:04 +0200 Subject: [PATCH 119/546] card view widget is able to change active state --- .../publisher/widgets/card_view_widgets.py | 49 +++++++++---------- .../publisher/widgets/overview_widget.py | 13 ----- .../tools/publisher/widgets/widgets.py | 1 - 3 files changed, 22 insertions(+), 41 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 67793bb50e..095a4eae7c 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -315,8 +315,9 @@ class CardWidget(BaseClickableFrame): def set_selected(self, selected): """Set card as selected.""" - if selected == self._selected: + if selected is self._selected: return + self._selected = selected state = "selected" if selected else "" self.setProperty("state", state) @@ -466,7 +467,7 @@ class InstanceCardWidget(CardWidget): self._active_checkbox = active_checkbox self._expand_btn = expand_btn - self._update_instance_context(context_info) + self._update_instance_values(context_info) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -475,23 +476,16 @@ class InstanceCardWidget(CardWidget): def is_active(self): return self._active_checkbox.isChecked() - def set_active(self, new_value): + def _set_active(self, new_value): """Set instance as active.""" checkbox_value = self._active_checkbox.isChecked() - instance_value = self.instance.is_active - - # First change instance value and them change checkbox - # - prevent to trigger `active_changed` signal - if instance_value != new_value: - self.instance.is_active = new_value - if checkbox_value != new_value: self._active_checkbox.setChecked(new_value) def update_instance(self, instance, context_info): """Update instance object and update UI.""" self.instance = instance - self._update_instance_context(context_info) + self._update_instance_values(context_info) def _validate_context(self, context_info): valid = context_info.is_valid @@ -527,10 +521,10 @@ class InstanceCardWidget(CardWidget): QtCore.Qt.NoTextInteraction ) - def _update_instance_context(self, context_info): + def _update_instance_values(self, context_info): """Update instance data""" self._update_product_name() - self.set_active(self.instance.is_active) + self._set_active(self.instance.is_active) self._validate_context(context_info) def _set_expanded(self, expanded=None): @@ -544,7 +538,6 @@ class InstanceCardWidget(CardWidget): if new_value == old_value: return - self.instance.is_active = new_value self.active_changed.emit(self._id, new_value) def _on_expend_clicked(self): @@ -630,24 +623,25 @@ class InstanceCardView(AbstractInstanceView): return widgets = self._get_selected_widgets() - changed = False + active_state_by_id = {} for widget in widgets: if not isinstance(widget, InstanceCardWidget): continue + instance_id = widget.id is_active = widget.is_active if value == -1: - widget.set_active(not is_active) - changed = True + active_state_by_id[instance_id] = not is_active continue _value = bool(value) if is_active is not _value: - widget.set_active(_value) - changed = True + active_state_by_id[instance_id] = _value - if changed: - self.active_changed.emit() + if not active_state_by_id: + return + + self._controller.set_instances_active_state(active_state_by_id) def keyPressEvent(self, event): if event.key() == QtCore.Qt.Key_Space: @@ -838,14 +832,15 @@ class InstanceCardView(AbstractInstanceView): def _on_active_changed(self, group_name, instance_id, value): group_widget = self._widgets_by_group[group_name] instance_widget = group_widget.get_widget_by_item_id(instance_id) - if instance_widget.is_selected: + active_state_by_id = {} + if not instance_widget.is_selected: + active_state_by_id[instance_id] = value + else: for widget in self._get_selected_widgets(): if isinstance(widget, InstanceCardWidget): - widget.set_active(value) - else: - self._select_item_clear(instance_id, group_name, instance_widget) - self.selection_changed.emit() - self.active_changed.emit() + active_state_by_id[widget.id] = value + + self._controller.set_instances_active_state(active_state_by_id) def _on_widget_selection(self, instance_id, group_name, selection_type): """Select specific item by instance id. diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index 5e8b803fc3..a09ee80ed5 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -15,7 +15,6 @@ from .product_info import ProductInfoWidget class OverviewWidget(QtWidgets.QFrame): - active_changed = QtCore.Signal() create_requested = QtCore.Signal() convert_requested = QtCore.Signal() publish_tab_requested = QtCore.Signal() @@ -125,13 +124,6 @@ class OverviewWidget(QtWidgets.QFrame): product_view_cards.double_clicked.connect( self.publish_tab_requested ) - # Active instances changed - product_list_view.active_changed.connect( - self._on_active_changed - ) - product_view_cards.active_changed.connect( - self._on_active_changed - ) # Instance context has changed product_attributes_widget.convert_requested.connect( self._on_convert_requested @@ -312,11 +304,6 @@ class OverviewWidget(QtWidgets.QFrame): instances, context_selected, convertor_identifiers ) - def _on_active_changed(self): - if self._refreshing_instances: - return - self.active_changed.emit() - def _on_change_anim(self, value): self._create_widget.setVisible(True) self._product_attributes_wrap.setVisible(True) diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 00c87ac249..a9d34c4c66 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -298,7 +298,6 @@ class ChangeViewBtn(PublishIconBtn): class AbstractInstanceView(QtWidgets.QWidget): """Abstract class for instance view in creation part.""" selection_changed = QtCore.Signal() - active_changed = QtCore.Signal() # Refreshed attribute is not changed by view itself # - widget which triggers `refresh` is changing the state # TODO store that information in widget which cares about refreshing From 8b5e3e7d77b4933dbe5b00b3101adade2a077f98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:54:34 +0200 Subject: [PATCH 120/546] implemented active state in list view --- .../publisher/widgets/list_view_widgets.py | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py index a8144e71f4..bc3353ba5e 100644 --- a/client/ayon_core/tools/publisher/widgets/list_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/list_view_widgets.py @@ -118,7 +118,7 @@ class InstanceListItemWidget(QtWidgets.QWidget): def __init__(self, instance, context_info, parent): super().__init__(parent) - self.instance = instance + self._instance_id = instance.id instance_label = instance.label if instance_label is None: @@ -171,47 +171,34 @@ class InstanceListItemWidget(QtWidgets.QWidget): def is_active(self): """Instance is activated.""" - return self.instance.is_active + return self._active_checkbox.isChecked() def set_active(self, new_value): """Change active state of instance and checkbox.""" - checkbox_value = self._active_checkbox.isChecked() - instance_value = self.instance.is_active + old_value = self.is_active() if new_value is None: - new_value = not instance_value + new_value = not old_value - # First change instance value and them change checkbox - # - prevent to trigger `active_changed` signal - if instance_value != new_value: - self.instance.is_active = new_value - - if checkbox_value != new_value: + if new_value != old_value: + self._active_checkbox.blockSignals(True) self._active_checkbox.setChecked(new_value) + self._active_checkbox.blockSignals(False) def update_instance(self, instance, context_info): """Update instance object.""" - self.instance = instance - self._update_instance_values(context_info) - - def _update_instance_values(self, context_info): - """Update instance data propagated to widgets.""" # Check product name - label = self.instance.label + label = instance.label if label != self._instance_label_widget.text(): self._instance_label_widget.setText(html_escape(label)) # Check active state - self.set_active(self.instance.is_active) + self.set_active(instance.is_active) # Check valid states self._set_valid_property(context_info.is_valid) def _on_active_change(self): - new_value = self._active_checkbox.isChecked() - old_value = self.instance.is_active - if new_value == old_value: - return - - self.instance.is_active = new_value - self.active_changed.emit(self.instance.id, new_value) + self.active_changed.emit( + self._instance_id, self._active_checkbox.isChecked() + ) def set_active_toggle_enabled(self, enabled): self._active_checkbox.setEnabled(enabled) @@ -892,20 +879,21 @@ class InstanceListView(AbstractInstanceView): def _on_active_changed(self, changed_instance_id, new_value): selected_instance_ids, _, _ = self.get_selected_items() - selected_ids = set() + active_by_id = {} found = False for instance_id in selected_instance_ids: - selected_ids.add(instance_id) + active_by_id[instance_id] = new_value if not found and instance_id == changed_instance_id: found = True if not found: - selected_ids = set() - selected_ids.add(changed_instance_id) + active_by_id = {changed_instance_id: new_value} - self._change_active_instances(selected_ids, new_value) + self._controller.set_instances_active_state(active_by_id) + + self._change_active_instances(active_by_id, new_value) group_names = set() - for instance_id in selected_ids: + for instance_id in active_by_id: group_name = self._group_by_instance_id.get(instance_id) if group_name is not None: group_names.add(group_name) @@ -917,16 +905,11 @@ class InstanceListView(AbstractInstanceView): if not instance_ids: return - changed_ids = set() for instance_id in instance_ids: widget = self._widgets_by_id.get(instance_id) if widget: - changed_ids.add(instance_id) widget.set_active(new_value) - if changed_ids: - self.active_changed.emit() - def _on_selection_change(self, *_args): self.selection_changed.emit() @@ -965,14 +948,16 @@ class InstanceListView(AbstractInstanceView): if not group_item: return - instance_ids = set() + active_by_id = {} for row in range(group_item.rowCount()): item = group_item.child(row) instance_id = item.data(INSTANCE_ID_ROLE) if instance_id is not None: - instance_ids.add(instance_id) + active_by_id[instance_id] = active - self._change_active_instances(instance_ids, active) + self._controller.set_instances_active_state(active_by_id) + + self._change_active_instances(active_by_id, active) proxy_index = self._proxy_model.mapFromSource(group_item.index()) if not self._instance_view.isExpanded(proxy_index): From 6a53253ca13b9e087861ae8d68824c56047d59ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:58:50 +0200 Subject: [PATCH 121/546] removed unused import --- client/ayon_core/tools/publisher/abstract.py | 1 - client/ayon_core/tools/publisher/models/create.py | 1 - 2 files changed, 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 4787e8a21b..a6ae93cecd 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -3,7 +3,6 @@ from typing import ( Optional, Dict, List, - Set, Tuple, Any, Callable, diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 2aa7b169a0..7cb46215df 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -4,7 +4,6 @@ from typing import ( Union, List, Dict, - Set, Tuple, Any, Optional, From f1a1e77134b5101d87715506d0ebc5591058b01f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:23:11 +0200 Subject: [PATCH 122/546] added new function to calculate representation delivery data --- client/ayon_core/pipeline/delivery.py | 80 ++++++++++++++++++++++- client/ayon_core/plugins/load/delivery.py | 33 ++++++---- 2 files changed, 100 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 029775e1db..174e81c194 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -3,11 +3,21 @@ import os import copy import shutil import glob -import clique import collections +from typing import List, Dict, Any, Iterable + +import clique +import ayon_api from ayon_core.lib import create_hard_link +from .anatomy import Anatomy +from .template_data import ( + get_general_template_data, + get_folder_template_data, + get_task_template_data, +) + def _copy_file(src_path, dst_path): """Hardlink file if possible(to save space), copy if not. @@ -327,3 +337,71 @@ def deliver_sequence( uploaded += 1 return report_items, uploaded + + +def _merge_data(data, new_data): + queue = collections.deque() + queue.append((data, new_data)) + while queue: + q_data, q_new_data = queue.popleft() + for key, value in q_new_data.items(): + if key in q_data and isinstance(value, dict): + queue.append((q_data[key], value)) + continue + q_data[key] = value + + +def get_representations_delivery_template_data( + project_name: str, + representation_ids: Iterable[str], +) -> Dict[str, Dict[str, Any]]: + representation_ids = set(representation_ids) + + output = { + repre_id: {} + for repre_id in representation_ids + } + if not representation_ids: + return output + + project_entity = ayon_api.get_project(project_name) + + general_template_data = get_general_template_data() + + repres_hierarchy = ayon_api.get_representations_hierarchy( + project_name, + representation_ids, + project_fields=set(), + folder_fields={"path", "folderType"}, + task_fields={"name", "taskType"}, + product_fields={"name", "productType"}, + version_fields={"version", "productId"}, + representation_fields=None, + ) + for repre_id, repre_hierarchy in repres_hierarchy.items(): + repre_entity = repre_hierarchy.representation + if repre_entity is None: + continue + + template_data = repre_entity["context"] + template_data.update(copy.deepcopy(general_template_data)) + template_data.update(get_folder_template_data( + repre_hierarchy.folder, project_name + )) + if repre_hierarchy.task: + template_data.update(get_task_template_data( + project_entity, repre_hierarchy.task + )) + + product_entity = repre_hierarchy.product + version_entity = repre_hierarchy.version + template_data.update({ + "product": { + "name": product_entity["name"], + "type": product_entity["productType"], + }, + "version": version_entity["version"], + }) + _merge_data(template_data, repre_entity["context"]) + output[repre_id] = template_data + return output diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 5c53d170eb..3c9f1b9691 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -200,20 +200,29 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): format_dict = get_format_dict(self.anatomy, self.root_line_edit.text()) renumber_frame = self.renumber_frame.isChecked() frame_offset = self.first_frame_start.value() + filtered_repres = [] + repre_ids = set() for repre in self._representations: - if repre["name"] not in selected_repres: - continue + if repre["name"] in selected_repres: + filtered_repres.append(repre) + repre_ids.add(repre["id"]) + template_data_by_repre_id = get_representations_template_data( + self.anatomy.project_name, repre_ids + ) + for repre in filtered_repres: repre_path = get_representation_path_with_anatomy( repre, self.anatomy ) - anatomy_data = copy.deepcopy(repre["context"]) - new_report_items = check_destination_path(repre["id"], - self.anatomy, - anatomy_data, - datetime_data, - template_name) + template_data = template_data_by_repre_id[repre["id"]] + new_report_items = check_destination_path( + repre["id"], + self.anatomy, + template_data, + datetime_data, + template_name + ) report_items.update(new_report_items) if new_report_items: @@ -224,7 +233,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): repre, self.anatomy, template_name, - anatomy_data, + template_data, format_dict, report_items, self.log @@ -267,9 +276,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): if frame is not None: if repre["context"].get("frame"): - anatomy_data["frame"] = frame + template_data["frame"] = frame elif repre["context"].get("udim"): - anatomy_data["udim"] = frame + template_data["udim"] = frame else: # Fallback self.log.warning( @@ -277,7 +286,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): " data. Supplying sequence frame to '{frame}'" " formatting data." ) - anatomy_data["frame"] = frame + template_data["frame"] = frame new_report_items, uploaded = deliver_single_file(*args) report_items.update(new_report_items) self._update_progress(uploaded) From ebdd757c669c6d72421abdb3df59742ad3eb4a03 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:55:24 +0200 Subject: [PATCH 123/546] fix imports --- client/ayon_core/pipeline/delivery.py | 3 +-- client/ayon_core/plugins/load/delivery.py | 7 +++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 174e81c194..2a2adf984a 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -4,14 +4,13 @@ import copy import shutil import glob import collections -from typing import List, Dict, Any, Iterable +from typing import Dict, Any, Iterable import clique import ayon_api from ayon_core.lib import create_hard_link -from .anatomy import Anatomy from .template_data import ( get_general_template_data, get_folder_template_data, diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 3c9f1b9691..e1cd136b26 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -1,23 +1,22 @@ -import copy import platform from collections import defaultdict import ayon_api from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.pipeline import load, Anatomy from ayon_core import resources, style - from ayon_core.lib import ( format_file_size, collect_frames, get_datetime_data, ) +from ayon_core.pipeline import load, Anatomy from ayon_core.pipeline.load import get_representation_path_with_anatomy from ayon_core.pipeline.delivery import ( get_format_dict, check_destination_path, - deliver_single_file + deliver_single_file, + get_representations_template_data, ) From e175121d0a30ccef65fcc1478c05740badc1dd99 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:55:38 +0200 Subject: [PATCH 124/546] fix typo --- client/ayon_core/plugins/load/delivery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index e1cd136b26..559950c997 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -350,8 +350,8 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): def _get_selected_repres(self): """Returns list of representation names filtered from checkboxes.""" selected_repres = [] - for repre_name, chckbox in self._representation_checkboxes.items(): - if chckbox.isChecked(): + for repre_name, checkbox in self._representation_checkboxes.items(): + if checkbox.isChecked(): selected_repres.append(repre_name) return selected_repres From d15740e33f0d3d652ff4c9bed1b82d852d316acc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 15 Oct 2024 15:30:50 +0200 Subject: [PATCH 125/546] fix import --- client/ayon_core/plugins/load/delivery.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 559950c997..406040d936 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -16,7 +16,7 @@ from ayon_core.pipeline.delivery import ( get_format_dict, check_destination_path, deliver_single_file, - get_representations_template_data, + get_representations_delivery_template_data, ) @@ -206,8 +206,10 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): filtered_repres.append(repre) repre_ids.add(repre["id"]) - template_data_by_repre_id = get_representations_template_data( - self.anatomy.project_name, repre_ids + template_data_by_repre_id = ( + get_representations_delivery_template_data( + self.anatomy.project_name, repre_ids + ) ) for repre in filtered_repres: repre_path = get_representation_path_with_anatomy( From ceedd7fbcd4489229aea76f6d64e89f76d233ea3 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 15 Oct 2024 14:22:34 +0000 Subject: [PATCH 126/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 458129f367..e9ce613942 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.1+dev" +__version__ = "1.0.2" diff --git a/package.py b/package.py index c059eed423..a9c66833cc 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.1+dev" +version = "1.0.2" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 0a7d0d76c9..293be52d6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.1+dev" +version = "1.0.2" description = "" authors = ["Ynput Team "] readme = "README.md" From 50cad97cad63474ff5a51d2fc2214cfb45d2766c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 15 Oct 2024 14:23:14 +0000 Subject: [PATCH 127/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index e9ce613942..8fa97eb9cb 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.2" +__version__ = "1.0.2+dev" diff --git a/package.py b/package.py index a9c66833cc..b5cd0a1903 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.2" +version = "1.0.2+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 293be52d6e..8b03020f6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.2" +version = "1.0.2+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From b239cdd8916e07c51bbed7ce7d024a911b554a4b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 16 Oct 2024 00:06:40 +0200 Subject: [PATCH 128/546] Fix single frame publishing (from e.g. Maya) Also fixes it for other hosts that use instance.data[`expectedFiles`] with the value being `list[dict[str, list[str]]]` (Basically the files per AOV, where the list of filenames is `list[str]` but the integrator and other areas really want a single `str` insteaf of `list[str]` if it's a single frame) --- client/ayon_core/pipeline/farm/pyblish_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index af90903bd8..98951b2766 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -788,15 +788,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, colorspace = product.colorspace break - if isinstance(files, (list, tuple)): - files = [os.path.basename(f) for f in files] + if isinstance(collected_files, (list, tuple)): + collected_files = [os.path.basename(f) for f in collected_files] else: - files = os.path.basename(files) + collected_files = os.path.basename(collected_files) rep = { "name": ext, "ext": ext, - "files": files, + "files": collected_files, "frameStart": int(skeleton["frameStartHandle"]), "frameEnd": int(skeleton["frameEndHandle"]), # If expectedFile are absolute, we need only filenames From 1eb25ef945ffe0fe279f0ad4e4e54ba818f85e00 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 16 Oct 2024 09:34:12 +0000 Subject: [PATCH 129/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 8fa97eb9cb..3da4af0b4e 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.2+dev" +__version__ = "1.0.3" diff --git a/package.py b/package.py index b5cd0a1903..a5319dd139 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.2+dev" +version = "1.0.3" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 8b03020f6d..9b2b13ffa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.2+dev" +version = "1.0.3" description = "" authors = ["Ynput Team "] readme = "README.md" From 47b5d90495563be2660d8b10b1b0479c3d1330ad Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 16 Oct 2024 09:34:49 +0000 Subject: [PATCH 130/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 3da4af0b4e..9a951d7fd4 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.3" +__version__ = "1.0.3+dev" diff --git a/package.py b/package.py index a5319dd139..5d5218748c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.3" +version = "1.0.3+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 9b2b13ffa8..ebf08be4a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.3" +version = "1.0.3+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 2f6ca5a2385bd3073961cf49bca220e9d3fb88b9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Oct 2024 18:35:27 +0200 Subject: [PATCH 131/546] Implemented explicit frames for simple files representations --- .../pipeline/farm/pyblish_functions.py | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 98951b2766..5908644dca 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -7,7 +7,7 @@ from copy import deepcopy import attr import ayon_api import clique -from ayon_core.lib import Logger +from ayon_core.lib import Logger, collect_frames from ayon_core.pipeline import get_current_project_name, get_representation_path from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline.farm.patterning import match_aov_pattern @@ -295,11 +295,17 @@ def _add_review_families(families): return families -def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, - skip_integration_repre_list, - do_not_add_review, - context, - color_managed_plugin): +def prepare_representations( + skeleton_data, + exp_files, + anatomy, + aov_filter, + skip_integration_repre_list, + do_not_add_review, + context, + color_managed_plugin, + frames_to_render +): """Create representations for file sequences. This will return representations of expected files if they are not @@ -315,6 +321,8 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, skip_integration_repre_list (list): exclude specific extensions, do_not_add_review (bool): explicitly skip review color_managed_plugin (publish.ColormanagedPyblishPluginMixin) + frames_to_render (str): implicit or explicit range of frames to render + this value is sent to Deadline in JobInfo.Frames Returns: list of representations @@ -325,6 +333,8 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, log = Logger.get_logger("farm_publishing") + frames_to_render = _get_real_frames_to_render(frames_to_render) + # create representation for every collected sequence for collection in collections: ext = collection.tail.lstrip(".") @@ -361,18 +371,21 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, " This may cause issues on farm." ).format(staging)) - frame_start = int(skeleton_data.get("frameStartHandle")) + frame_start = int(frames_to_render[0]) + frame_end = int(frames_to_render[-1]) if skeleton_data.get("slate"): frame_start -= 1 + files = _get_real_files_to_rendered(collection, frames_to_render) + # explicitly disable review by user preview = preview and not do_not_add_review rep = { "name": ext, "ext": ext, - "files": [os.path.basename(f) for f in list(collection)], + "files": files, "frameStart": frame_start, - "frameEnd": int(skeleton_data.get("frameEndHandle")), + "frameEnd": frame_end, # If expectedFile are absolute, we need only filenames "stagingDir": staging, "fps": skeleton_data.get("fps"), @@ -413,10 +426,13 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, " This may cause issues on farm." ).format(staging)) + files = _get_real_files_to_rendered( + [os.path.basename(remainder)], frames_to_render) + rep = { "name": ext, "ext": ext, - "files": os.path.basename(remainder), + "files": files[0], "stagingDir": staging, } @@ -453,6 +469,49 @@ def prepare_representations(skeleton_data, exp_files, anatomy, aov_filter, return representations +def _get_real_frames_to_render(frames): + """Returns list of frames that should be rendered. + + Artists could want to selectively render only particular frames + """ + frames_to_render = [] + for frame in frames.split(","): + if "-" in frame: + splitted = frame.split("-") + frames_to_render.extend(range(int(splitted[0]), int(splitted[1]))) + else: + frames_to_render.append(frame) + return [str(frame_to_render) for frame_to_render in frames_to_render] + + +def _get_real_files_to_rendered(collection, frames_to_render): + """Use expected files based on real frames_to_render. + + Artists might explicitly set frames they want to render via Publisher UI. + This uses this value to filter out files + Args: + frames_to_render (list): of str '1001' + """ + files = [os.path.basename(f) for f in list(collection)] + file_name, extracted_frame = list(collect_frames(files).items())[0] + if extracted_frame: + found_frame_pattern_length = len(extracted_frame) + normalized_frames_to_render = set() + for frame_to_render in frames_to_render: + normalized_frames_to_render.add( + str(frame_to_render).zfill(found_frame_pattern_length) + ) + + filtered_files = [] + for file_name in files: + if any(frame in file_name + for frame in normalized_frames_to_render): + filtered_files.append(file_name) + + files = filtered_files + return files + + def create_instances_for_aov(instance, skeleton, aov_filter, skip_integration_repre_list, do_not_add_review): From c93f3449b6b419eb429b641a65449e7bc44c68ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:15:49 +0200 Subject: [PATCH 132/546] ignore publish attributes without attribute definitions --- client/ayon_core/tools/publisher/models/create.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 7cb46215df..9c13d8ae2f 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -27,6 +27,7 @@ from ayon_core.pipeline.create import ( Creator, CreateContext, CreatedInstance, + AttributeValues, ) from ayon_core.pipeline.create import ( CreatorsOperationFailed, @@ -857,7 +858,10 @@ class CreateModel: item_id = None if isinstance(item, CreatedInstance): item_id = item.id + for plugin_name, attr_val in item.publish_attributes.items(): + if not isinstance(attr_val, AttributeValues): + continue attr_defs = attr_val.attr_defs if not attr_defs: continue From 202800a48749e74f9234ac86d2a4778e170ea33e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:35:11 +0200 Subject: [PATCH 133/546] added helper function to get task type for an instance --- client/ayon_core/pipeline/create/context.py | 58 +++++++++++++++++---- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index adbb03a820..6ec2ea284c 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -254,7 +254,7 @@ class CreateContext: # Context validation cache self._folder_id_by_folder_path = {} - self._task_names_by_folder_path = {} + self._task_infos_by_folder_path = {} self.thumbnail_paths_by_instance_id = {} @@ -567,7 +567,7 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} self._folder_id_by_folder_path = {} - self._task_names_by_folder_path = {} + self._task_infos_by_folder_path = {} self._event_hub.clear_callbacks() def reset_finalization(self): @@ -1468,6 +1468,42 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) + def get_instances_task_type( + self, instances: Optional[Iterable["CreatedInstance"]] = None + ) -> Dict[str, Optional[str]]: + """Helper function to get task type of task on instance. + + Based on context set on instance (using 'folderPath' and 'task') tries + to find task type. + + Task type is 'None' if task is not set or is not valid, and + is always 'None' for instances with promised context. + + Args: + instances (Optional[Iterable[CreatedInstance]]): Instances to + get task type. If not provided all instances are used. + + Returns: + Dict[str, Optional[str]]: Task type by instance id. + + """ + context_infos = self.get_instances_context_info(instances) + output = {} + for instance_id, context_info in context_infos.items(): + folder_path = context_info.folder_path + task_name = context_info.task_name + if not task_name or not folder_path: + output[instance_id] = None + continue + task_info = ( + self._task_infos_by_folder_path.get(folder_path) or {} + ).get(task_name) + task_type = None + if task_info is not None: + task_type = task_info["task_type"] + output[instance_id] = task_type + return output + def get_instances_context_info( self, instances: Optional[Iterable["CreatedInstance"]] = None ) -> Dict[str, InstanceContextInfo]: @@ -1508,6 +1544,7 @@ class CreateContext: if instance.has_promised_context: context_info.folder_is_valid = True context_info.task_is_valid = True + # NOTE missing task type continue # TODO allow context promise folder_path = context_info.folder_path @@ -1522,7 +1559,7 @@ class CreateContext: task_name = context_info.task_name if task_name is not None: - tasks_cache = self._task_names_by_folder_path.get(folder_path) + tasks_cache = self._task_infos_by_folder_path.get(folder_path) if tasks_cache is not None: context_info.task_is_valid = task_name in tasks_cache continue @@ -1574,15 +1611,17 @@ class CreateContext: tasks_entities = ayon_api.get_tasks( project_name, folder_ids=folder_paths_by_id.keys(), - fields={"name", "folderId"} + fields={"name", "folderId", "taskType"} ) - task_names_by_folder_path = collections.defaultdict(set) + task_infos_by_folder_path = collections.defaultdict(dict) for task_entity in tasks_entities: folder_id = task_entity["folderId"] folder_path = folder_paths_by_id[folder_id] - task_names_by_folder_path[folder_path].add(task_entity["name"]) - self._task_names_by_folder_path.update(task_names_by_folder_path) + task_infos_by_folder_path[folder_path][task_entity["name"]] = { + "task_type": task_entity["taskType"], + } + self._task_infos_by_folder_path.update(task_infos_by_folder_path) for instance in to_validate: folder_path = instance["folderPath"] @@ -1593,15 +1632,16 @@ class CreateContext: folder_path = folder_entities[0]["path"] instance["folderPath"] = folder_path - if folder_path not in task_names_by_folder_path: + if folder_path not in task_infos_by_folder_path: continue context_info = info_by_instance_id[instance.id] context_info.folder_is_valid = True if ( not task_name - or task_name in task_names_by_folder_path[folder_path] + or task_name in task_infos_by_folder_path[folder_path] ): + task_info = task_infos_by_folder_path[folder_path] context_info.task_is_valid = True return info_by_instance_id From d0ed7f086e05a7bf5633b04ebb23b81ec5659e11 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:41:29 +0200 Subject: [PATCH 134/546] small improvement --- client/ayon_core/pipeline/create/context.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6ec2ea284c..f8e277dccf 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1490,17 +1490,19 @@ class CreateContext: context_infos = self.get_instances_context_info(instances) output = {} for instance_id, context_info in context_infos.items(): - folder_path = context_info.folder_path task_name = context_info.task_name - if not task_name or not folder_path: + if not task_name or not context_info.is_valid: output[instance_id] = None continue - task_info = ( - self._task_infos_by_folder_path.get(folder_path) or {} - ).get(task_name) + task_type = None - if task_info is not None: - task_type = task_info["task_type"] + tasks_cache = self._task_infos_by_folder_path.get( + context_info.folder_path + ) + if tasks_cache is not None: + task_info = tasks_cache.get(task_name) + if task_info is not None: + task_type = task_info["task_type"] output[instance_id] = task_type return output From 385e5bd02c551d8d0c340dbab9d50d3ba3f18047 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 16:46:44 +0200 Subject: [PATCH 135/546] change name of method --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f8e277dccf..36bef9f88b 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1468,7 +1468,7 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def get_instances_task_type( + def get_instances_task_types( self, instances: Optional[Iterable["CreatedInstance"]] = None ) -> Dict[str, Optional[str]]: """Helper function to get task type of task on instance. From 0e8b129d6af008397e48a850544b18b75065722b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 23 Oct 2024 14:49:16 +0000 Subject: [PATCH 136/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 9a951d7fd4..47da5b3a1b 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.3+dev" +__version__ = "1.0.4" diff --git a/package.py b/package.py index 5d5218748c..0ba9303182 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.3+dev" +version = "1.0.4" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ebf08be4a8..64b389ea3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.3+dev" +version = "1.0.4" description = "" authors = ["Ynput Team "] readme = "README.md" From d2ee4167ae0151908eda349d582663bf193efdd9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 23 Oct 2024 14:49:58 +0000 Subject: [PATCH 137/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 47da5b3a1b..8a7065c93c 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.4" +__version__ = "1.0.4+dev" diff --git a/package.py b/package.py index 0ba9303182..7c5bffe81f 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.4" +version = "1.0.4+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 64b389ea3e..c686d685fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.4" +version = "1.0.4+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 1ce9bcf9d2ea3a6235ef8b1737e09b749966c302 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:21:05 +0200 Subject: [PATCH 138/546] revert some changes --- client/ayon_core/pipeline/create/context.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 36bef9f88b..ac3851a51a 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -254,7 +254,7 @@ class CreateContext: # Context validation cache self._folder_id_by_folder_path = {} - self._task_infos_by_folder_path = {} + self._task_names_by_folder_path = {} self.thumbnail_paths_by_instance_id = {} @@ -567,7 +567,7 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} self._folder_id_by_folder_path = {} - self._task_infos_by_folder_path = {} + self._task_names_by_folder_path = {} self._event_hub.clear_callbacks() def reset_finalization(self): @@ -1561,7 +1561,7 @@ class CreateContext: task_name = context_info.task_name if task_name is not None: - tasks_cache = self._task_infos_by_folder_path.get(folder_path) + tasks_cache = self._task_names_by_folder_path.get(folder_path) if tasks_cache is not None: context_info.task_is_valid = task_name in tasks_cache continue @@ -1613,17 +1613,16 @@ class CreateContext: tasks_entities = ayon_api.get_tasks( project_name, folder_ids=folder_paths_by_id.keys(), - fields={"name", "folderId", "taskType"} + fields={"name", "folderId"} ) - task_infos_by_folder_path = collections.defaultdict(dict) + task_names_by_folder_path = collections.defaultdict(set) for task_entity in tasks_entities: folder_id = task_entity["folderId"] folder_path = folder_paths_by_id[folder_id] - task_infos_by_folder_path[folder_path][task_entity["name"]] = { - "task_type": task_entity["taskType"], - } - self._task_infos_by_folder_path.update(task_infos_by_folder_path) + task_names_by_folder_path[folder_path].add(task_entity["name"]) + + self._task_names_by_folder_path.update(task_names_by_folder_path) for instance in to_validate: folder_path = instance["folderPath"] From 360a8b39b078714a32255a9d27925bd2f57c54ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:37:47 +0200 Subject: [PATCH 139/546] implemented logic to cache entities --- client/ayon_core/pipeline/create/context.py | 422 +++++++++++++++----- 1 file changed, 324 insertions(+), 98 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index ac3851a51a..7c9449cb21 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -12,6 +12,7 @@ from typing import ( Iterable, Tuple, List, + Set, Dict, Any, Callable, @@ -252,8 +253,11 @@ class CreateContext: # Shared data across creators during collection phase self._collection_shared_data = None - # Context validation cache + # Entities cache + self._folder_entities_by_id = {} + self._task_entities_by_id = {} self._folder_id_by_folder_path = {} + self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} self.thumbnail_paths_by_instance_id = {} @@ -356,12 +360,12 @@ class CreateContext: return self._host_is_valid @property - def host_name(self): + def host_name(self) -> str: if hasattr(self.host, "name"): return self.host.name return os.environ["AYON_HOST_NAME"] - def get_current_project_name(self): + def get_current_project_name(self) -> Optional[str]: """Project name which was used as current context on context reset. Returns: @@ -370,7 +374,7 @@ class CreateContext: return self._current_project_name - def get_current_folder_path(self): + def get_current_folder_path(self) -> Optional[str]: """Folder path which was used as current context on context reset. Returns: @@ -379,7 +383,7 @@ class CreateContext: return self._current_folder_path - def get_current_task_name(self): + def get_current_task_name(self) -> Optional[str]: """Task name which was used as current context on context reset. Returns: @@ -388,7 +392,7 @@ class CreateContext: return self._current_task_name - def get_current_task_type(self): + def get_current_task_type(self) -> Optional[str]: """Task type which was used as current context on context reset. Returns: @@ -403,7 +407,7 @@ class CreateContext: self._current_task_type = task_type return self._current_task_type - def get_current_project_entity(self): + def get_current_project_entity(self) -> Optional[Dict[str, Any]]: """Project entity for current context project. Returns: @@ -419,26 +423,21 @@ class CreateContext: self._current_project_entity = project_entity return copy.deepcopy(self._current_project_entity) - def get_current_folder_entity(self): + def get_current_folder_entity(self) -> Optional[Dict[str, Any]]: """Folder entity for current context folder. Returns: - Union[dict[str, Any], None]: Folder entity. + Optional[dict[str, Any]]: Folder entity. """ if self._current_folder_entity is not _NOT_SET: return copy.deepcopy(self._current_folder_entity) - folder_entity = None + folder_path = self.get_current_folder_path() - if folder_path: - project_name = self.get_current_project_name() - folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path - ) - self._current_folder_entity = folder_entity + self._current_folder_entity = self.get_folder_entity(folder_path) return copy.deepcopy(self._current_folder_entity) - def get_current_task_entity(self): + def get_current_task_entity(self) -> Optional[Dict[str, Any]]: """Task entity for current context task. Returns: @@ -447,18 +446,12 @@ class CreateContext: """ if self._current_task_entity is not _NOT_SET: return copy.deepcopy(self._current_task_entity) - task_entity = None + + folder_path = self.get_current_folder_path() task_name = self.get_current_task_name() - if task_name: - folder_entity = self.get_current_folder_entity() - if folder_entity: - project_name = self.get_current_project_name() - task_entity = ayon_api.get_task_by_name( - project_name, - folder_id=folder_entity["id"], - task_name=task_name - ) - self._current_task_entity = task_entity + self._current_task_entity = self.get_task_entity( + folder_path, task_name + ) return copy.deepcopy(self._current_task_entity) def get_current_workfile_path(self): @@ -566,8 +559,14 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} + + self._folder_entities_by_id = {} + self._task_entities_by_id = {} + self._folder_id_by_folder_path = {} + self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} + self._event_hub.clear_callbacks() def reset_finalization(self): @@ -1468,42 +1467,265 @@ class CreateContext: if failed_info: raise CreatorsCreateFailed(failed_info) - def get_instances_task_types( + def get_folder_entities(self, folder_paths: Iterable[str]): + """Get folder entities by paths. + + Args: + folder_paths (Iterable[str]): Folder paths. + + Returns: + Dict[str, Optional[Dict[str, Any]]]: Folder entities by path. + + """ + output = { + folder_path: None + for folder_path in folder_paths + if folder_path is not None + } + remainders = set() + for folder_path in output: + # Skip empty/invalid folder paths + if folder_path is None or "/" not in folder_path: + continue + + if folder_path not in self._folder_id_by_folder_path: + remainders.add(folder_path) + continue + + folder_id = self._folder_id_by_folder_path.get(folder_path) + if not folder_id: + output[folder_path] = None + continue + + folder_entity = self._folder_entities_by_id.get(folder_id) + if folder_entity: + output[folder_path] = folder_entity + else: + remainders.add(folder_path) + + if not remainders: + return output + + folder_paths_by_id = {} + for folder_entity in ayon_api.get_folders( + self.project_name, + folder_paths=remainders, + ): + folder_id = folder_entity["id"] + folder_path = folder_entity["path"] + folder_paths_by_id[folder_id] = folder_path + output[folder_path] = folder_entity + self._folder_entities_by_id[folder_id] = folder_entity + self._folder_id_by_folder_path[folder_path] = folder_id + + return output + + def get_task_entities( + self, + task_names_by_folder_paths: Dict[str, Set[str]] + ) -> Dict[str, Dict[str, Optional[Dict[str, Any]]]]: + """Get task entities by folder path and task name. + + Entities are cached until reset. + + Args: + task_names_by_folder_paths (Dict[str, Set[str]]): Task names by + folder path. + + Returns: + Dict[str, Dict[str, Dict[str, Any]]]: Task entities by folder path + and task name. + + """ + output = {} + for folder_path, task_names in task_names_by_folder_paths.items(): + if folder_path is None: + continue + output[folder_path] = { + task_name: None + for task_name in task_names + if task_name is not None + } + + missing_folder_paths = set() + for folder_path, output_task_entities_by_name in output.items(): + if not output_task_entities_by_name: + continue + + if folder_path not in self._task_ids_by_folder_path: + missing_folder_paths.add(folder_path) + continue + + all_tasks_filled = True + task_ids = self._task_ids_by_folder_path[folder_path] + task_entities_by_name = {} + for task_id in task_ids: + task_entity = self._task_entities_by_id.get(task_id) + if task_entity is None: + all_tasks_filled = False + continue + task_entities_by_name[task_entity["name"]] = task_entity + + any_missing = False + for task_name in set(output_task_entities_by_name): + task_entity = task_entities_by_name.get(task_name) + if task_entity is None: + any_missing = True + continue + + output_task_entities_by_name[task_name] = task_entity + + if any_missing and not all_tasks_filled: + missing_folder_paths.add(folder_path) + + if not missing_folder_paths: + return output + + folder_entities_by_path = self.get_folder_entities( + missing_folder_paths + ) + folder_ids = set() + for folder_path, folder_entity in folder_entities_by_path.items(): + if folder_entity is not None: + folder_ids.add(folder_entity["id"]) + + if not folder_ids: + return output + + task_entities_by_parent_id = collections.defaultdict(list) + for task_entity in ayon_api.get_tasks( + self.project_name, + folder_ids=folder_ids + ): + folder_id = task_entity["folderId"] + task_entities_by_parent_id[folder_id].append(task_entity) + + for folder_id, task_entities in task_entities_by_parent_id.items(): + folder_path = self._folder_id_by_folder_path.get(folder_id) + task_ids = set() + task_names = set() + for task_entity in task_entities: + task_id = task_entity["id"] + task_name = task_entity["name"] + task_ids.add(task_id) + task_names.add(task_name) + self._task_entities_by_id[task_id] = task_entity + + output[folder_path][task_name] = task_entity + self._task_ids_by_folder_path[folder_path] = task_ids + self._task_names_by_folder_path[folder_path] = task_names + + return output + + def get_folder_entity( + self, + folder_path: Optional[str], + ) -> Optional[Dict[str, Any]]: + """Get folder entity by path. + + Entities are cached until reset. + + Args: + folder_path (Optional[str]): Folder path. + + Returns: + Optional[Dict[str, Any]]: Folder entity. + + """ + if not folder_path: + return None + return self.get_folder_entities([folder_path]).get(folder_path) + + def get_task_entity( + self, + folder_path: Optional[str], + task_name: Optional[str], + ) -> Optional[Dict[str, Any]]: + """Get task entity by name and folder path. + + Entities are cached until reset. + + Args: + folder_path (Optional[str]): Folder path. + task_name (Optional[str]): Task name. + + Returns: + Optional[Dict[str, Any]]: Task entity. + + """ + if not folder_path or not task_name: + return None + + output = self.get_task_entities({folder_path: {task_name}}) + return output.get(folder_path, {}).get(task_name) + + def get_instances_folder_entities( self, instances: Optional[Iterable["CreatedInstance"]] = None - ) -> Dict[str, Optional[str]]: - """Helper function to get task type of task on instance. + ) -> Dict[str, Optional[Dict[str, Any]]]: + if instances is None: + instances = self._instances_by_id.values() + instances = list(instances) + output = { + instance.id: None + for instance in instances + } + if not instances: + return output - Based on context set on instance (using 'folderPath' and 'task') tries - to find task type. + folder_paths = { + instance.get("folderPath") + for instance in instances + } + folder_entities_by_path = self.get_folder_entities(folder_paths) + for instance in instances: + folder_path = instance.get("folderPath") + output[instance.id] = folder_entities_by_path.get(folder_path) + return output - Task type is 'None' if task is not set or is not valid, and - is always 'None' for instances with promised context. + def get_instances_task_entities( + self, instances: Optional[Iterable["CreatedInstance"]] = None + ): + """Get task entities for instances. Args: instances (Optional[Iterable[CreatedInstance]]): Instances to - get task type. If not provided all instances are used. + get task entities. If not provided all instances are used. Returns: - Dict[str, Optional[str]]: Task type by instance id. + Dict[str, Optional[Dict[str, Any]]]: Task entity by instance id. """ - context_infos = self.get_instances_context_info(instances) - output = {} - for instance_id, context_info in context_infos.items(): - task_name = context_info.task_name - if not task_name or not context_info.is_valid: - output[instance_id] = None - continue + if instances is None: + instances = self._instances_by_id.values() + instances = list(instances) - task_type = None - tasks_cache = self._task_infos_by_folder_path.get( - context_info.folder_path + output = { + instance.id: None + for instance in instances + } + if not instances: + return output + + filtered_instances = [] + task_names_by_folder_path = collections.defaultdict(set) + for instance in instances: + folder_path = instance.get("folderPath") + task_name = instance.get("task") + if not folder_path or not task_name: + continue + filtered_instances.append(instance) + task_names_by_folder_path[folder_path].add(task_name) + + task_entities_by_folder_path = self.get_task_entities( + task_names_by_folder_path + ) + for instance in filtered_instances: + folder_path = instance["folderPath"] + task_name = instance["task"] + output[instance.id] = ( + task_entities_by_folder_path[folder_path][task_name] ) - if tasks_cache is not None: - task_info = tasks_cache.get(task_name) - if task_info is not None: - task_type = task_info["task_type"] - output[instance_id] = task_type + return output def get_instances_context_info( @@ -1574,75 +1796,79 @@ class CreateContext: # Backwards compatibility for cases where folder name is set instead # of folder path - folder_names = set() folder_paths = set() - for folder_path in task_names_by_folder_path.keys(): + task_names_by_folder_name = {} + task_names_by_folder_path_clean = {} + for folder_path, task_names in task_names_by_folder_path.items(): if folder_path is None: - pass - elif "/" in folder_path: - folder_paths.add(folder_path) - else: - folder_names.add(folder_path) + continue - folder_paths_by_id = {} - if folder_paths: + clean_task_names = { + task_name + for task_name in task_names + if task_name + } + + if "/" not in folder_path: + task_names_by_folder_name[folder_path] = clean_task_names + continue + + folder_paths.add(folder_path) + if not clean_task_names: + continue + + task_names_by_folder_path_clean[folder_path] = clean_task_names + + folder_paths_by_name = collections.defaultdict(list) + if task_names_by_folder_name: for folder_entity in ayon_api.get_folders( project_name, - folder_paths=folder_paths, - fields={"id", "path"} + folder_names=task_names_by_folder_name.keys(), + fields={"name", "path"} ): - folder_id = folder_entity["id"] - folder_path = folder_entity["path"] - folder_paths_by_id[folder_id] = folder_path - self._folder_id_by_folder_path[folder_path] = folder_id - - folder_entities_by_name = collections.defaultdict(list) - if folder_names: - for folder_entity in ayon_api.get_folders( - project_name, - folder_names=folder_names, - fields={"id", "name", "path"} - ): - folder_id = folder_entity["id"] folder_name = folder_entity["name"] folder_path = folder_entity["path"] - folder_paths_by_id[folder_id] = folder_path - folder_entities_by_name[folder_name].append(folder_entity) - self._folder_id_by_folder_path[folder_path] = folder_id + folder_paths_by_name[folder_name].append(folder_path) - tasks_entities = ayon_api.get_tasks( - project_name, - folder_ids=folder_paths_by_id.keys(), - fields={"name", "folderId"} + folder_path_by_name = {} + for folder_name, paths in folder_paths_by_name.items(): + if len(paths) != 1: + continue + path = paths[0] + folder_path_by_name[folder_name] = path + folder_paths.add(path) + clean_task_names = task_names_by_folder_name[folder_name] + if not clean_task_names: + continue + folder_task_names = task_names_by_folder_path_clean.setdefault( + path, set() + ) + folder_task_names |= clean_task_names + + folder_entities_by_path = self.get_folder_entities(folder_paths) + task_entities_by_folder_path = self.get_task_entities( + task_names_by_folder_path_clean ) - task_names_by_folder_path = collections.defaultdict(set) - for task_entity in tasks_entities: - folder_id = task_entity["folderId"] - folder_path = folder_paths_by_id[folder_id] - task_names_by_folder_path[folder_path].add(task_entity["name"]) - - self._task_names_by_folder_path.update(task_names_by_folder_path) - for instance in to_validate: folder_path = instance["folderPath"] task_name = instance.get("task") if folder_path and "/" not in folder_path: - folder_entities = folder_entities_by_name.get(folder_path) - if len(folder_entities) == 1: - folder_path = folder_entities[0]["path"] - instance["folderPath"] = folder_path + new_folder_path = folder_path_by_name.get(folder_path) + if new_folder_path: + folder_path = new_folder_path + instance["folderPath"] = new_folder_path - if folder_path not in task_infos_by_folder_path: + folder_entity = folder_entities_by_path.get(folder_path) + if not folder_entity: continue context_info = info_by_instance_id[instance.id] context_info.folder_is_valid = True if ( not task_name - or task_name in task_infos_by_folder_path[folder_path] + or task_name in task_entities_by_folder_path[folder_path] ): - task_info = task_infos_by_folder_path[folder_path] context_info.task_is_valid = True return info_by_instance_id From 92e43fc45b81a9305589be85871011cf3236012c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Oct 2024 23:44:24 +0200 Subject: [PATCH 140/546] Fix variable names + simplify logic --- .../extract_usd_layer_contributions.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index a67c6ec702..0ffce8b643 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -464,17 +464,13 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, return [] # Attributes logic - disabled = False publish_attributes = instance["publish_attributes"].get( cls.__name__, {}) - enabled = publish_attributes.get("contribution_enabled", True) - variant_enabled = enabled and publish_attributes.get( + visible = publish_attributes.get("contribution_enabled", True) + variant_visible = visible and publish_attributes.get( "contribution_apply_as_variant", True) - disabled = not enabled - variant_disabled = not variant_enabled - return [ UISeparatorDef("usd_container_settings1"), UILabelDef(label="USD Contribution"), @@ -500,7 +496,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "department layer." ), default="usdAsset", - hidden=disabled), + visible=visible), EnumDef("contribution_target_product_init", label="Initialize as", tooltip=( @@ -512,7 +508,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, ), items=["asset", "shot"], default="asset", - hidden=disabled), + visible=visible), # Asset layer, e.g. model.usd, look.usd, rig.usd EnumDef("contribution_layer", @@ -525,7 +521,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, ), items=list(cls.contribution_layers.keys()), default="model", - hidden=disabled), + visible=visible), BoolDef("contribution_apply_as_variant", label="Add as variant", tooltip=( @@ -537,15 +533,15 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "instead." ), default=True, - hidden=disabled), + visible=visible), TextDef("contribution_variant_set_name", label="Variant Set Name", default="{layer}", - hidden=variant_disabled), + visible=variant_visible), TextDef("contribution_variant", label="Variant Name", default="{variant}", - hidden=variant_disabled), + visible=variant_visible), BoolDef("contribution_variant_is_default", label="Set as default variant selection", tooltip=( @@ -557,7 +553,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "for the same variant set have this enabled." ), default=False, - hidden=variant_disabled), + visible=variant_visible), UISeparatorDef("usd_container_settings3"), ] From 2b5ab5439aa7d33b5c70ca11ce476cadae81ba0f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Oct 2024 13:41:20 +0200 Subject: [PATCH 141/546] returning empty lines --- client/ayon_core/pipeline/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index d5c3140d37..505c847c36 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -120,12 +120,16 @@ __all__ = ( "AYON_CONTAINER_ID", "AYON_INSTANCE_ID", "HOST_WORKFILE_EXTENSIONS", + # --- Anatomy --- "Anatomy", + # --- Temp dir --- "get_temp_dir", + # --- Staging dir --- "get_staging_dir", + # --- Create --- "BaseCreator", "Creator", @@ -134,6 +138,7 @@ __all__ = ( "CreatedInstance", "CreatorError", "CreatorError", + # - legacy creation "LegacyCreator", "legacy_create", @@ -143,6 +148,7 @@ __all__ = ( "deregister_creator_plugin", "register_creator_plugin_path", "deregister_creator_plugin_path", + # --- Load --- "HeroVersionType", "IncompatibleLoaderError", @@ -161,6 +167,7 @@ __all__ = ( "get_representation_path", "get_representation_context", "get_repres_contexts", + # --- Publish --- "PublishValidationError", "PublishXmlValidationError", @@ -168,6 +175,7 @@ __all__ = ( "AYONPyblishPluginMixin", "OpenPypePyblishPluginMixin", "OptionalPyblishPluginMixin", + # --- Actions --- "LauncherAction", "InventoryAction", @@ -179,6 +187,7 @@ __all__ = ( "register_inventory_action_path", "deregister_inventory_action", "deregister_inventory_action_path", + # --- Process context --- "install_ayon_plugins", "install_openpype_plugins", @@ -197,12 +206,14 @@ __all__ = ( "get_current_project_name", "get_current_folder_path", "get_current_task_name", + # Workfile templates "discover_workfile_build_plugins", "register_workfile_build_plugin", "deregister_workfile_build_plugin", "register_workfile_build_plugin_path", "deregister_workfile_build_plugin_path", + # Backwards compatible function names "install", "uninstall", From 2b765954a3451bc6cba358584b64fed9e8f633e6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Oct 2024 13:54:05 +0200 Subject: [PATCH 142/546] returning empty lines --- client/ayon_core/pipeline/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 505c847c36..ea8b1617c6 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -137,11 +137,13 @@ __all__ = ( "HiddenCreator", "CreatedInstance", "CreatorError", + "CreatorError", # - legacy creation "LegacyCreator", "legacy_create", + "discover_creator_plugins", "discover_legacy_creator_plugins", "register_creator_plugin", @@ -154,15 +156,18 @@ __all__ = ( "IncompatibleLoaderError", "LoaderPlugin", "ProductLoaderPlugin", + "discover_loader_plugins", "register_loader_plugin", "deregister_loader_plugin_path", "register_loader_plugin_path", "deregister_loader_plugin", + "load_container", "remove_container", "update_container", "switch_container", + "loaders_from_representation", "get_representation_path", "get_representation_context", @@ -179,9 +184,11 @@ __all__ = ( # --- Actions --- "LauncherAction", "InventoryAction", + "discover_launcher_actions", "register_launcher_action", "register_launcher_action_path", + "discover_inventory_actions", "register_inventory_action", "register_inventory_action_path", @@ -194,12 +201,15 @@ __all__ = ( "install_host", "uninstall_host", "is_installed", + "register_root", "registered_root", + "register_host", "registered_host", "deregister_host", "get_process_id", + "get_global_context", "get_current_context", "get_current_host_name", From 396af0cf8610c2d2991c8a7842f164dcc49d98f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 24 Oct 2024 13:52:43 +0200 Subject: [PATCH 143/546] Update client/ayon_core/pipeline/create/creator_plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/create/creator_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 1360a74519..32ac2bd61f 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -800,7 +800,7 @@ class Creator(BaseCreator): product_name = instance.get("productName") product_type = instance.get("productType") folder_path = instance.get("folderPath") - if not any([product_name, folder_path]): + if not product_name or not folder_path: return None version = instance.get("version") From 9a860785bbce917cc2064db74a018ab012922d64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 24 Oct 2024 13:52:57 +0200 Subject: [PATCH 144/546] Update client/ayon_core/pipeline/create/creator_plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/create/creator_plugins.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 32ac2bd61f..0de7707e38 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -828,8 +828,7 @@ class Creator(BaseCreator): # TODO: not sure if this is necessary # path might be already created by get_staging_dir - if not os.path.exists(staging_dir_path): - os.makedirs(staging_dir_path) + os.makedirs(staging_dir_path, exist_ok=True) instance.transient_data.update(staging_dir_data) From bd03634ed1a3237e49f1d3307a96722d0960b2a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 24 Oct 2024 13:53:38 +0200 Subject: [PATCH 145/546] Update client/ayon_core/pipeline/publish/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/publish/lib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index fb4db6ddf1..ba56c38c78 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -642,7 +642,15 @@ def get_custom_staging_dir_info( log=None, ): from ayon_core.pipeline.stagingdir import get_staging_dir_config - + warnings.warn( + ( + "Function 'get_custom_staging_dir_info' in" + " 'ayon_core.pipeline.publish' is deprecated. Please use" + " 'get_custom_staging_dir_info'" + " in 'ayon_core.pipeline.stagingdir'." + ), + DeprecationWarning, + ) tr_data = get_staging_dir_config( host_name, project_name, From fedf8e60c7b33f5873538773561269e686ee81b4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Oct 2024 13:56:45 +0200 Subject: [PATCH 146/546] Add warnings module for future use Imported the 'warnings' module for potential future usage in the codebase. --- client/ayon_core/pipeline/publish/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index ba56c38c78..657af9570b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -2,6 +2,7 @@ import os import sys import inspect import copy +import warnings import xml.etree.ElementTree from typing import Optional, Union, List From ea23f355f6dcb24f2dcb2806878afa27ddd11907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 24 Oct 2024 14:00:36 +0200 Subject: [PATCH 147/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/publish/lib.py | 3 +-- client/ayon_core/pipeline/stagingdir.py | 20 +++++++------------- client/ayon_core/pipeline/tempdir.py | 15 ++++++--------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 657af9570b..6a31da82b2 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -721,8 +721,7 @@ def get_instance_staging_dir(instance): # TODO: not sure if this is necessary # path might be already created by get_staging_dir - if not os.path.exists(staging_dir_path): - os.makedirs(staging_dir_path) + os.makedirs(staging_dir_path, exist_ok=True) instance.data.update(staging_dir_data) diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index 5ab9596528..d0172c4848 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -1,9 +1,9 @@ from ayon_core.lib import Logger, filter_profiles, StringTemplate from ayon_core.settings import get_project_settings -from .anatomy import Anatomy -from .tempdir import get_temp_dir from ayon_core.pipeline.template_data import get_template_data +from .anatomy import Anatomy +from .tempdir import get_temp_dir STAGING_DIR_TEMPLATES = "staging" @@ -34,8 +34,10 @@ def get_staging_dir_config( Returns: Dict or None: Data with directory template and is_persistent or None + Raises: ValueError - if misconfigured template should be used + """ settings = project_settings or get_project_settings(project_name) @@ -91,14 +93,6 @@ def _validate_template_name(project_name, template_name, anatomy): Raises: ValueError - if misconfigured template """ - # TODO: only for backward compatibility of anatomy for older projects - if STAGING_DIR_TEMPLATES not in anatomy.templates: - raise ValueError( - ( - 'Anatomy of project "{}" does not have set' ' "{}" template section!' - ).format(project_name, template_name) - ) - if template_name not in anatomy.templates[STAGING_DIR_TEMPLATES]: raise ValueError( ( @@ -147,9 +141,9 @@ def get_staging_dir( template. Returns: - Dict[str, Any]: Staging dir data - """ + Optional[Dict[str, Any]]: Staging dir data + """ log = kwargs.get("log") or Logger.get_logger("get_staging_dir") always_return_path = kwargs.get("always_return_path") @@ -209,7 +203,7 @@ def get_staging_dir( ), "stagingDir_persistent": False, } - elif not staging_dir_config: + if not staging_dir_config: return None return { diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index a6328135ee..448e774e7c 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -23,24 +23,21 @@ def get_temp_dir( Template formatting is supported also with optional keys. Folder is created in case it doesn't exists. - Available anatomy formatting keys: - - root[work | ] - - project[name | code] - Note: Staging dir does not have to be necessarily in tempdir so be careful about its usage. Args: - project_name (str)[optional]: Name of project. - anatomy (openpype.pipeline.Anatomy)[optional]: Anatomy object. - make_local (bool)[optional]: If True, temp dir will be created in + project_name (str): Name of project. + anatomy (Optional[Anatomy]): Project Anatomy object. + suffix (Optional[str]): Suffix for tempdir. + prefix (Optional[str]): Prefix for tempdir. + make_local (Optional[bool]): If True, temp dir will be created in local tempdir. - suffix (str)[optional]: Suffix for tempdir. - prefix (str)[optional]: Prefix for tempdir. Returns: str: Path to staging dir of instance. + """ prefix = prefix or "ay_tmp_" suffix = suffix or "" From ea4ec677cac7a73f14225722f0dbc9c17328ff6a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Oct 2024 14:40:30 +0200 Subject: [PATCH 148/546] reviewer suggestions for changes - Renamed function `get_staging_dir` to `get_staging_dir_info` for clarity. - Updated variable names in multiple files to use `template_data` instead of `formatting_data`. - Adjusted function parameters in the staging directory module for consistency and added new optional parameters. - Improved logging by passing logger instances instead of creating new loggers within functions. --- client/ayon_core/pipeline/__init__.py | 4 +- .../pipeline/create/creator_plugins.py | 18 ++++----- client/ayon_core/pipeline/publish/lib.py | 17 ++++----- client/ayon_core/pipeline/stagingdir.py | 34 ++++++++++------- client/ayon_core/pipeline/tempdir.py | 37 ++++++++----------- 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index ea8b1617c6..4060501a92 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -10,7 +10,7 @@ from .anatomy import Anatomy from .tempdir import get_temp_dir -from .stagingdir import get_staging_dir +from .stagingdir import get_staging_dir_info from .create import ( BaseCreator, @@ -128,7 +128,7 @@ __all__ = ( "get_temp_dir", # --- Staging dir --- - "get_staging_dir", + "get_staging_dir_info", # --- Create --- "BaseCreator", diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 0de7707e38..124395ae16 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -15,7 +15,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path ) -from ayon_core.pipeline import get_staging_dir +from ayon_core.pipeline import get_staging_dir_info from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -805,9 +805,9 @@ class Creator(BaseCreator): version = instance.get("version") if version is not None: - formatting_data = {"version": version} + template_data = {"version": version} - staging_dir_data = get_staging_dir( + staging_dir_info = get_staging_dir_info( create_ctx.host_name, create_ctx.get_current_project_entity(), create_ctx.get_current_folder_entity(), @@ -817,20 +817,20 @@ class Creator(BaseCreator): create_ctx.get_current_project_anatomy(), create_ctx.get_current_project_settings(), always_return_path=False, - log=self.log, - formatting_data=formatting_data, + logger=self.log, + template_data=template_data, ) - if not staging_dir_data: + if not staging_dir_info: return None - staging_dir_path = staging_dir_data["stagingDir"] + staging_dir_path = staging_dir_info["stagingDir"] # TODO: not sure if this is necessary - # path might be already created by get_staging_dir + # path might be already created by get_staging_dir_info os.makedirs(staging_dir_path, exist_ok=True) - instance.transient_data.update(staging_dir_data) + instance.transient_data.update(staging_dir_info) self.log.info(f"Applied staging dir to instance: {staging_dir_path}") diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 6a31da82b2..0f3a7c1d45 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -18,7 +18,7 @@ from ayon_core.lib import ( ) from ayon_core.settings import get_project_settings from ayon_core.addon import AddonsManager -from ayon_core.pipeline import get_staging_dir +from ayon_core.pipeline import get_staging_dir_info from ayon_core.pipeline.plugin_discover import DiscoverResult from .constants import ( DEFAULT_PUBLISH_TEMPLATE, @@ -685,7 +685,7 @@ def get_instance_staging_dir(instance): return staging_dir anatomy_data = instance.data["anatomyData"] - formatting_data = copy.deepcopy(anatomy_data) + template_data = copy.deepcopy(anatomy_data) product_type = instance.data["productType"] product_name = instance.data["productName"] @@ -703,9 +703,9 @@ def get_instance_staging_dir(instance): if current_file: workfile = os.path.basename(current_file) workfile_name, _ = os.path.splitext(workfile) - formatting_data["workfile_name"] = workfile_name + template_data["workfile_name"] = workfile_name - staging_dir_data = get_staging_dir( + staging_dir_info = get_staging_dir_info( host_name, project_entity, folder_entity, @@ -714,16 +714,15 @@ def get_instance_staging_dir(instance): product_name, anatomy, project_settings=project_settings, - formatting_data=formatting_data, + template_data=template_data, ) - staging_dir_path = staging_dir_data["stagingDir"] + staging_dir_path = staging_dir_info["stagingDir"] - # TODO: not sure if this is necessary - # path might be already created by get_staging_dir + # path might be already created by get_staging_dir_info os.makedirs(staging_dir_path, exist_ok=True) - instance.data.update(staging_dir_data) + instance.data.update(staging_dir_info) return staging_dir_path diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index d0172c4848..e9d425cf28 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -102,7 +102,7 @@ def _validate_template_name(project_name, template_name, anatomy): ) -def get_staging_dir( +def get_staging_dir_info( host_name, project_entity, folder_entity, @@ -111,9 +111,13 @@ def get_staging_dir( product_name, anatomy, project_settings=None, + template_data=None, + always_return_path=None, + force_tmp_dir=None, + logger=None, **kwargs ): - """Get staging dir data. + """Get staging dir info data. If `force_temp` is set, staging dir will be created as tempdir. If `always_get_some_dir` is set, staging dir will be created as tempdir if @@ -129,29 +133,31 @@ def get_staging_dir( product_name (str): Name of product. anatomy (ayon_core.pipeline.Anatomy): Anatomy object. project_settings (Optional[Dict[str, Any]]): Prepared project settings. + template_data (Optional[Dict[str, Any]]): Data for formatting staging + dir template. + always_return_path (Optional[bool]): If True, staging dir will be + created as tempdir if no staging dir profile is found. Input value + False will return None if no staging dir profile is found. + force_tmp_dir (Optional[bool]): If True, staging dir will be created as + tempdir. + logger (Optional[logging.Logger]): Logger instance. **kwargs: Arbitrary keyword arguments. See below. Keyword Arguments: - force_temp (bool): If True, staging dir will be created as tempdir. - always_return_path (bool): If True, staging dir will be created as - tempdir if no staging dir profile is found. prefix (str): Prefix for staging dir. suffix (str): Suffix for staging dir. - formatting_data (Dict[str, Any]): Data for formatting staging dir - template. Returns: - Optional[Dict[str, Any]]: Staging dir data + Optional[Dict[str, Any]]: Staging dir info data """ - log = kwargs.get("log") or Logger.get_logger("get_staging_dir") - always_return_path = kwargs.get("always_return_path") + log = logger or Logger.get_logger("get_staging_dir_info") # make sure always_return_path is set to true by default if always_return_path is None: always_return_path = True - if kwargs.get("force_temp"): + if force_tmp_dir: return get_temp_dir( project_name=project_entity["name"], anatomy=anatomy, @@ -175,9 +181,9 @@ def get_staging_dir( "host": host_name, }) - # add additional data from kwargs - if kwargs.get("formatting_data"): - ctx_data.update(kwargs.get("formatting_data")) + # add additional template formatting data + if template_data: + ctx_data.update(template_data) # get staging dir config staging_dir_config = get_staging_dir_config( diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index 448e774e7c..440ed882aa 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -10,29 +10,25 @@ from ayon_core.pipeline import Anatomy def get_temp_dir( - project_name=None, anatomy=None, prefix=None, suffix=None, make_local=False + project_name, anatomy=None, prefix=None, suffix=None, use_local_temp=False ): """Get temporary dir path. - If `make_local` is set, tempdir will be created in local tempdir. + If `use_local_temp` is set, tempdir will be created in local tempdir. If `anatomy` is not set, default anatomy will be used. If `prefix` or `suffix` is not set, default values will be used. - It also supports `OPENPYPE_TMPDIR`, so studio can define own temp + It also supports `AYON_TMPDIR`, so studio can define own temp shared repository per project or even per more granular context. Template formatting is supported also with optional keys. Folder is created in case it doesn't exists. - Note: - Staging dir does not have to be necessarily in tempdir so be careful - about its usage. - Args: project_name (str): Name of project. anatomy (Optional[Anatomy]): Project Anatomy object. suffix (Optional[str]): Suffix for tempdir. prefix (Optional[str]): Prefix for tempdir. - make_local (Optional[bool]): If True, temp dir will be created in + use_local_temp (Optional[bool]): If True, temp dir will be created in local tempdir. Returns: @@ -42,7 +38,7 @@ def get_temp_dir( prefix = prefix or "ay_tmp_" suffix = suffix or "" - if make_local: + if use_local_temp: return _create_local_staging_dir(prefix, suffix) # make sure anatomy is set @@ -55,19 +51,20 @@ def get_temp_dir( return _create_local_staging_dir(prefix, suffix, custom_temp_dir) -def _create_local_staging_dir(prefix, suffix, dir=None): +def _create_local_staging_dir(prefix, suffix, dirpath=None): """Create local staging dir Args: prefix (str): prefix for tempdir suffix (str): suffix for tempdir + dirpath (Optional[str]): path to tempdir Returns: str: path to tempdir """ # use pathlib for creating tempdir staging_dir = Path(tempfile.mkdtemp( - prefix=prefix, suffix=suffix, dir=dir + prefix=prefix, suffix=suffix, dir=dirpath )) return staging_dir.as_posix() @@ -89,31 +86,27 @@ def _create_custom_tempdir(project_name, anatomy=None): Returns: str | None: formatted path or None """ - env_tmpdir = os.getenv("AYON_TMPDIR") + env_tmpdir = os.getenv( + "AYON_TMPDIR", + ) if not env_tmpdir: - env_tmpdir = os.getenv("OPENPYPE_TMPDIR") - if not env_tmpdir: - return - print( - "DEPRECATION WARNING: Used 'OPENPYPE_TMPDIR' environment" - " variable. Please use 'AYON_TMPDIR' instead." - ) + return None custom_tempdir = None if "{" in env_tmpdir: if anatomy is None: anatomy = Anatomy(project_name) # create base formate data - template_formatting_data = { + template_data = { "root": anatomy.roots, "project": { "name": anatomy.project_name, "code": anatomy.project_code, - } + }, } # path is anatomy template custom_tempdir = StringTemplate.format_template( - env_tmpdir, template_formatting_data) + env_tmpdir, template_data) custom_tempdir_path = Path(custom_tempdir) From 1eb09045832f29024b0b2dba00adc6d71ace132f Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Oct 2024 14:48:47 +0200 Subject: [PATCH 149/546] Remove unnecessary root addition in get_staging_dir_info function. The code changes remove the unnecessary addition of roots to ctx_data in the get_staging_dir_info function. --- client/ayon_core/pipeline/stagingdir.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index e9d425cf28..818acef36a 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -169,8 +169,6 @@ def get_staging_dir_info( ctx_data = get_template_data( project_entity, folder_entity, task_entity, host_name ) - # add roots to ctx_data - ctx_data["root"] = anatomy.roots # add additional data ctx_data.update({ @@ -178,7 +176,7 @@ def get_staging_dir_info( "type": product_type, "name": product_name }, - "host": host_name, + "root": anatomy.roots }) # add additional template formatting data From 9c9f02f0d264536bb1acdb6ee7f02cd3e5ed990c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:51:31 +0200 Subject: [PATCH 150/546] implemented iter --- client/ayon_core/pipeline/create/structures.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index bcc9a87c49..ba4a373597 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -132,6 +132,10 @@ class AttributeValues: def __contains__(self, key): return key in self._attr_defs_by_key + def __iter__(self): + for key in self._attr_defs_by_key: + yield key + def get(self, key, default=None): if key in self._attr_defs_by_key: return self[key] From fa9af2f8ded7e9c89519cfde2f01e04a3dd8b58a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 24 Oct 2024 15:06:11 +0200 Subject: [PATCH 151/546] Refactor Creator class method to handle missing info better. - Updated return type in docstring - Added a comment for clarity - Removed unnecessary return statements --- client/ayon_core/pipeline/create/creator_plugins.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 801feedd3d..4cbf432efd 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -843,14 +843,16 @@ class Creator(BaseCreator): dir applied. Returns: - str: Path to staging dir. + Optional[str]: Staging dir path or None if not applied. """ create_ctx = self.create_context product_name = instance.get("productName") product_type = instance.get("productType") folder_path = instance.get("folderPath") + + # this can only work if product name and folder path are available if not product_name or not folder_path: - return None + return version = instance.get("version") if version is not None: @@ -871,7 +873,7 @@ class Creator(BaseCreator): ) if not staging_dir_info: - return None + return staging_dir_path = staging_dir_info["stagingDir"] From 11bb657d35921c42759339937ee8b82230235e98 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 24 Oct 2024 22:10:25 +0200 Subject: [PATCH 152/546] Do not try to continue with logic if the instance isn't valid for the plug-in anyway --- .../plugins/publish/extract_usd_layer_contributions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 0ffce8b643..180cb8bbf1 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -600,6 +600,10 @@ class CollectUSDLayerContributionsHoudiniLook(CollectUSDLayerContributions): @classmethod def get_attr_defs_for_instance(cls, create_context, instance): + # Filtering of instance, if needed, can be customized + if not cls.instance_matches_plugin_families(instance): + return [] + defs = super().get_attr_defs_for_instance(create_context, instance) # Update default for department layer to look From a28f4959e6daa156defac5ad379a4abaf0105237 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 25 Oct 2024 14:40:36 +0200 Subject: [PATCH 153/546] replace html tags with markdown --- server/settings/publish_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index cdcd28a9ce..16b1f37187 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -459,8 +459,8 @@ class ExtractReviewFilterModel(BaseSettingsModel): single_frame_filter: str = SettingsField( "everytime", # codespell:ignore everytime description=( - "Use output always / only if input is 1 frame" - " image / only if has 2+ frames or is video" + "Use output **always** / only if input **is 1 frame**" + " image / only if has **2+ frames** or **is video**" ), enum_resolver=extract_review_filter_enum ) From 54bb5c51b9eb58252f28987eeed5db6aec30a3a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 25 Oct 2024 17:00:27 +0200 Subject: [PATCH 154/546] udated release trigger action --- .github/workflows/release_trigger.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/release_trigger.yml b/.github/workflows/release_trigger.yml index 01a3b3a682..4293e4a8e9 100644 --- a/.github/workflows/release_trigger.yml +++ b/.github/workflows/release_trigger.yml @@ -2,10 +2,23 @@ name: 🚀 Release Trigger on: workflow_dispatch: + inputs: + draft: + type: boolean + description: "Create Release Draft" + required: false + default: false + release_overwrite: + type: string + description: "Set Version Release Tag" + required: false jobs: call-release-trigger: uses: ynput/ops-repo-automation/.github/workflows/release_trigger.yml@main + with: + draft: ${{ inputs.draft }} + release_overwrite: ${{ inputs.release_overwrite }} secrets: token: ${{ secrets.YNPUT_BOT_TOKEN }} email: ${{ secrets.CI_EMAIL }} From 8208bef6f31933a1b0aef5151dc2b957e712fe03 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 25 Oct 2024 23:06:12 +0200 Subject: [PATCH 155/546] Allow to target not only `productType` by default with attributes, but also by `families` data on created instance --- client/ayon_core/pipeline/publish/publish_plugins.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index d2c70894cc..6a2f4c0279 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -205,9 +205,9 @@ class AYONPyblishPluginMixin: if not cls.__instanceEnabled__: return False - for _ in pyblish.logic.plugins_by_families( - [cls], [instance.product_type] - ): + families = [instance.product_type] + families.extend(instance.data.get("families", [])) + for _ in pyblish.logic.plugins_by_families([cls], families): return True return False From 9595d8fe91e27cb046f053eb86711520826c5848 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Sun, 27 Oct 2024 09:39:08 +0100 Subject: [PATCH 156/546] fix serialize of regex --- client/ayon_core/lib/attribute_definitions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 4877a45118..5daf646873 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -523,7 +523,10 @@ class TextDef(AbstractAttrDef): def serialize(self): data = super().serialize() - data["regex"] = self.regex.pattern + regex = None + if self.regex is not None: + regex = self.regex.pattern + data["regex"] = regex data["multiline"] = self.multiline data["placeholder"] = self.placeholder return data From ea2a9e0221bbfff11ce1c75e9f000ec5d4f1d33b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 28 Oct 2024 14:56:22 +0100 Subject: [PATCH 157/546] fix also label clone --- client/ayon_core/lib/attribute_definitions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 5daf646873..34956fd33f 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -327,8 +327,8 @@ class UISeparatorDef(UIDef): class UILabelDef(UIDef): type = "label" - def __init__(self, label, key=None): - super().__init__(label=label, key=key) + def __init__(self, label, key=None, *args, **kwargs): + super().__init__(label=label, key=key, *args, **kwargs) def _def_type_compare(self, other: "UILabelDef") -> bool: return self.label == other.label From 37f2a6bb33931d2b8c536ab0c2831c8987d58648 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 28 Oct 2024 17:43:06 +0100 Subject: [PATCH 158/546] use kwarg to pass subtype --- .../ayon_core/plugins/publish/extract_hierarchy_to_ayon.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index 60c92aa8b1..a169affc66 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -154,7 +154,9 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): # TODO check if existing entity have 'task' type if task_entity is None: task_entity = entity_hub.add_new_task( - task_info["type"], + task_type=task_info["type"], + # TODO change 'parent_id' to 'folder_id' when ayon api + # is updated parent_id=entity.id, name=task_name ) @@ -182,7 +184,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): folder_type = "Folder" child_entity = entity_hub.add_new_folder( - folder_type, + folder_type=folder_type, parent_id=entity.id, name=child_name ) From 90a9ffa4758e841251af398004326748c71df9f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:20:03 +0100 Subject: [PATCH 159/546] fix representation entity --- client/ayon_core/pipeline/delivery.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 2a2adf984a..de89c8eec2 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -383,6 +383,13 @@ def get_representations_delivery_template_data( continue template_data = repre_entity["context"] + # Bug in 'ayon_api', 'get_representations_hierarchy' did not fully + # convert representation entity. Fixed in 'ayon_api' 1.0.10 . + if isinstance(template_data, str): + con = ayon_api.get_server_api_connection() + repre_entity = con._representation_conversion(repre_entity) + template_data = repre_entity["context"] + template_data.update(copy.deepcopy(general_template_data)) template_data.update(get_folder_template_data( repre_hierarchy.folder, project_name From 30a3aeaa86c4a6d6496b7887269069a2ea543691 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:31:00 +0100 Subject: [PATCH 160/546] remove space between version and dot --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index de89c8eec2..1a8a8498b9 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -384,7 +384,7 @@ def get_representations_delivery_template_data( template_data = repre_entity["context"] # Bug in 'ayon_api', 'get_representations_hierarchy' did not fully - # convert representation entity. Fixed in 'ayon_api' 1.0.10 . + # convert representation entity. Fixed in 'ayon_api' 1.0.10. if isinstance(template_data, str): con = ayon_api.get_server_api_connection() repre_entity = con._representation_conversion(repre_entity) From e3022d01786a6cd8ff90ae5816c658cd91f467c2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 30 Oct 2024 12:24:12 +0000 Subject: [PATCH 161/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 8a7065c93c..5a3281ed01 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.4+dev" +__version__ = "1.0.5" diff --git a/package.py b/package.py index 7c5bffe81f..ec21628d04 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.4+dev" +version = "1.0.5" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index c686d685fb..6bdffc663e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.4+dev" +version = "1.0.5" description = "" authors = ["Ynput Team "] readme = "README.md" From 1af3b2d242ee10e13712afc101523a46703c9eef Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 30 Oct 2024 12:25:01 +0000 Subject: [PATCH 162/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 5a3281ed01..b2480af462 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.5" +__version__ = "1.0.5+dev" diff --git a/package.py b/package.py index ec21628d04..38d930189f 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.5" +version = "1.0.5+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 6bdffc663e..31acb3f8b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.5" +version = "1.0.5+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From d2c7f1db2d7f3d016bca707cb376948030ba973c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:05:35 +0100 Subject: [PATCH 163/546] remove `"root"` key from template data --- client/ayon_core/pipeline/delivery.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 1a8a8498b9..8fdf2547ba 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -409,5 +409,9 @@ def get_representations_delivery_template_data( "version": version_entity["version"], }) _merge_data(template_data, repre_entity["context"]) + + # Remove roots from temlate data to auto-fill them with anatomy data + template_data.pop("root") + output[repre_id] = template_data return output From f8d0f2ac36e07e397862ec594c570a2409dbfb6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:30:21 +0100 Subject: [PATCH 164/546] fix typo Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 8fdf2547ba..05b78dc05b 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -410,7 +410,7 @@ def get_representations_delivery_template_data( }) _merge_data(template_data, repre_entity["context"]) - # Remove roots from temlate data to auto-fill them with anatomy data + # Remove roots from template data to auto-fill them with anatomy data template_data.pop("root") output[repre_id] = template_data From e4d0d3dfb95ea519903fb60562b8d1b9cf4f705b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:31:22 +0100 Subject: [PATCH 165/546] safe pop of root --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 05b78dc05b..366c261e08 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -411,7 +411,7 @@ def get_representations_delivery_template_data( _merge_data(template_data, repre_entity["context"]) # Remove roots from template data to auto-fill them with anatomy data - template_data.pop("root") + template_data.pop("root", None) output[repre_id] = template_data return output From 0c64785f2749ffb60f35287e323f0b6d67b3b007 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:02:52 +0100 Subject: [PATCH 166/546] fix mapping to folder path --- client/ayon_core/pipeline/create/context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 7c9449cb21..14133cd18b 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1584,24 +1584,24 @@ class CreateContext: folder_entities_by_path = self.get_folder_entities( missing_folder_paths ) - folder_ids = set() + folder_path_by_id = {} for folder_path, folder_entity in folder_entities_by_path.items(): if folder_entity is not None: - folder_ids.add(folder_entity["id"]) + folder_path_by_id[folder_entity["id"]] = folder_path - if not folder_ids: + if not folder_path_by_id: return output task_entities_by_parent_id = collections.defaultdict(list) for task_entity in ayon_api.get_tasks( self.project_name, - folder_ids=folder_ids + folder_ids=folder_path_by_id.keys() ): folder_id = task_entity["folderId"] task_entities_by_parent_id[folder_id].append(task_entity) for folder_id, task_entities in task_entities_by_parent_id.items(): - folder_path = self._folder_id_by_folder_path.get(folder_id) + folder_path = folder_path_by_id[folder_id] task_ids = set() task_names = set() for task_entity in task_entities: From f48425dd47dbb953c62c87d94a8a807486657d9d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:03:00 +0100 Subject: [PATCH 167/546] better variable name --- client/ayon_core/pipeline/create/context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 14133cd18b..4b7e323737 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1482,14 +1482,14 @@ class CreateContext: for folder_path in folder_paths if folder_path is not None } - remainders = set() + remainder_paths = set() for folder_path in output: # Skip empty/invalid folder paths if folder_path is None or "/" not in folder_path: continue if folder_path not in self._folder_id_by_folder_path: - remainders.add(folder_path) + remainder_paths.add(folder_path) continue folder_id = self._folder_id_by_folder_path.get(folder_path) @@ -1501,15 +1501,15 @@ class CreateContext: if folder_entity: output[folder_path] = folder_entity else: - remainders.add(folder_path) + remainder_paths.add(folder_path) - if not remainders: + if not remainder_paths: return output folder_paths_by_id = {} for folder_entity in ayon_api.get_folders( self.project_name, - folder_paths=remainders, + folder_paths=remainder_paths, ): folder_id = folder_entity["id"] folder_path = folder_entity["path"] From 31c37efce2501fbdf1cc78e2ce7d2b75e4ca1b46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:18:43 +0100 Subject: [PATCH 168/546] use single variable to cache folders --- client/ayon_core/pipeline/create/context.py | 39 ++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 4b7e323737..d7fdad6fdb 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -254,9 +254,8 @@ class CreateContext: self._collection_shared_data = None # Entities cache - self._folder_entities_by_id = {} + self._folder_entities_by_path = {} self._task_entities_by_id = {} - self._folder_id_by_folder_path = {} self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} @@ -560,10 +559,9 @@ class CreateContext: # Give ability to store shared data for collection phase self._collection_shared_data = {} - self._folder_entities_by_id = {} + self._folder_entities_by_path = {} self._task_entities_by_id = {} - self._folder_id_by_folder_path = {} self._task_ids_by_folder_path = {} self._task_names_by_folder_path = {} @@ -1485,38 +1483,31 @@ class CreateContext: remainder_paths = set() for folder_path in output: # Skip empty/invalid folder paths - if folder_path is None or "/" not in folder_path: + if "/" not in folder_path: continue - if folder_path not in self._folder_id_by_folder_path: + if folder_path not in self._folder_entities_by_path: remainder_paths.add(folder_path) continue - folder_id = self._folder_id_by_folder_path.get(folder_path) - if not folder_id: - output[folder_path] = None - continue - - folder_entity = self._folder_entities_by_id.get(folder_id) - if folder_entity: - output[folder_path] = folder_entity - else: - remainder_paths.add(folder_path) + output[folder_path] = self._folder_entities_by_path[folder_path] if not remainder_paths: return output - folder_paths_by_id = {} + found_paths = set() for folder_entity in ayon_api.get_folders( self.project_name, folder_paths=remainder_paths, ): - folder_id = folder_entity["id"] folder_path = folder_entity["path"] - folder_paths_by_id[folder_id] = folder_path + found_paths.add(folder_path) output[folder_path] = folder_entity - self._folder_entities_by_id[folder_id] = folder_entity - self._folder_id_by_folder_path[folder_path] = folder_id + self._folder_entities_by_path[folder_path] = folder_entity + + # Cache empty folders + for path in remainder_paths - found_paths: + self._folder_entities_by_path[path] = None return output @@ -1775,9 +1766,9 @@ class CreateContext: if not folder_path: continue - if folder_path in self._folder_id_by_folder_path: - folder_id = self._folder_id_by_folder_path[folder_path] - if folder_id is None: + if folder_path in self._folder_entities_by_path: + folder_entity = self._folder_entities_by_path[folder_path] + if folder_entity is None: continue context_info.folder_is_valid = True From d860919b22cb7832df2963bf9c23d2aabfcad919 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:21:31 +0100 Subject: [PATCH 169/546] remove unnecessary check --- client/ayon_core/pipeline/create/context.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index d7fdad6fdb..a0145dee29 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1478,11 +1478,11 @@ class CreateContext: output = { folder_path: None for folder_path in folder_paths - if folder_path is not None } remainder_paths = set() for folder_path in output: - # Skip empty/invalid folder paths + # Skip invalid folder paths (e.g. if only folder name + # is passed in) if "/" not in folder_path: continue @@ -1505,7 +1505,7 @@ class CreateContext: output[folder_path] = folder_entity self._folder_entities_by_path[folder_path] = folder_entity - # Cache empty folders + # Cache empty folder entities for path in remainder_paths - found_paths: self._folder_entities_by_path[path] = None From ce4e8a1b04acb42bfd81ca65b4f17d6270e940ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:22:47 +0100 Subject: [PATCH 170/546] handle empty paths --- client/ayon_core/pipeline/create/context.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a0145dee29..3cff5e03b1 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1481,9 +1481,8 @@ class CreateContext: } remainder_paths = set() for folder_path in output: - # Skip invalid folder paths (e.g. if only folder name - # is passed in) - if "/" not in folder_path: + # Skip invalid folder paths (folder name or empty path) + if not folder_path or "/" not in folder_path: continue if folder_path not in self._folder_entities_by_path: From b4009b718ae6bdcfe398361dc3cd4f1176cfcee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:23:15 +0100 Subject: [PATCH 171/546] discard None from folder paths --- client/ayon_core/pipeline/create/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 3cff5e03b1..6bfd64b822 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1666,6 +1666,7 @@ class CreateContext: instance.get("folderPath") for instance in instances } + folder_paths.discard(None) folder_entities_by_path = self.get_folder_entities(folder_paths) for instance in instances: folder_path = instance.get("folderPath") From eb561dd371cb0ae7825ac2fc281ffb7fd1cfe720 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:07:31 +0100 Subject: [PATCH 172/546] Remove `os.link` compatibility for Windows. Support for Windows exists since Py 3.2 See: https://docs.python.org/3/library/os.html#os.link --- client/ayon_core/lib/path_tools.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index 5c81fbfebf..efe2556afe 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -38,31 +38,7 @@ def create_hard_link(src_path, dst_path): dst_path(str): Full path to a file where a link of source will be added. """ - # Use `os.link` if is available - # - should be for all platforms with newer python versions - if hasattr(os, "link"): - os.link(src_path, dst_path) - return - - # Windows implementation of hardlinks - # - used in Python 2 - if platform.system().lower() == "windows": - import ctypes - from ctypes.wintypes import BOOL - CreateHardLink = ctypes.windll.kernel32.CreateHardLinkW - CreateHardLink.argtypes = [ - ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_void_p - ] - CreateHardLink.restype = BOOL - - res = CreateHardLink(dst_path, src_path, None) - if res == 0: - raise ctypes.WinError() - return - # Raises not implemented error if gets here - raise NotImplementedError( - "Implementation of hardlink for current environment is missing." - ) + os.link(src_path, dst_path) def collect_frames(files): From 0a970abed6bcccb2755f70af189959ca00c6b3d9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:11:52 +0100 Subject: [PATCH 173/546] Remove Python 2 code. `unicode` does not exist in Py3+ --- client/ayon_core/lib/log.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 36c39f9d84..1ed16b36ff 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -11,12 +11,12 @@ import copy from . import Terminal -# Check for `unicode` in builtins -USE_UNICODE = hasattr(__builtins__, "unicode") - class LogStreamHandler(logging.StreamHandler): - """ StreamHandler class designed to handle utf errors in python 2.x hosts. + """StreamHandler class. + + This was originally designed to handle UTF errors in python 2.x hosts, + however currently solely remains for backwards compatibility. """ @@ -47,27 +47,7 @@ class LogStreamHandler(logging.StreamHandler): stream = self.stream if stream is None: return - fs = "%s\n" - # if no unicode support... - if not USE_UNICODE: - stream.write(fs % msg) - else: - try: - if (isinstance(msg, unicode) and # noqa: F821 - getattr(stream, 'encoding', None)): - ufs = u'%s\n' - try: - stream.write(ufs % msg) - except UnicodeEncodeError: - stream.write((ufs % msg).encode(stream.encoding)) - else: - if (getattr(stream, 'encoding', 'utf-8')): - ufs = u'%s\n' - stream.write(ufs % unicode(msg)) # noqa: F821 - else: - stream.write(fs % msg) - except UnicodeError: - stream.write(fs % msg.encode("UTF-8")) + stream.write(f"{msg}\n") self.flush() except (KeyboardInterrupt, SystemExit): raise From 9f9c03179a6ab3bec5dfe3f8e757be6baf8cc055 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:16:16 +0100 Subject: [PATCH 174/546] Fix enabled check + fix docstrings --- client/ayon_core/lib/log.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 1ed16b36ff..d619310eb8 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -25,21 +25,21 @@ class LogStreamHandler(logging.StreamHandler): self.enabled = True def enable(self): - """ Enable StreamHandler + """Enable StreamHandler - Used to silence output + Make StreamHandler output again """ self.enabled = True def disable(self): - """ Disable StreamHandler + """Disable StreamHandler - Make StreamHandler output again + Used to silence output """ self.enabled = False def emit(self, record): - if not self.enable: + if not self.enabled: return try: msg = self.format(record) From 9343e3cca98bf9a3f546efdfd77b43a5e78d9da3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:20:27 +0100 Subject: [PATCH 175/546] Remove `Logger.mongo_process_id` --- client/ayon_core/lib/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index 36c39f9d84..03edc93c74 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -141,8 +141,6 @@ class Logger: process_data = None # Cached process name or ability to set different process name _process_name = None - # TODO Remove 'mongo_process_id' in 1.x.x - mongo_process_id = uuid.uuid4().hex @classmethod def get_logger(cls, name=None): From 300e086c8d4075b39e5627e1a68e7cfa6a6c8793 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:28:59 +0100 Subject: [PATCH 176/546] Update client/ayon_core/lib/log.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index d619310eb8..d392610f32 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -39,7 +39,7 @@ class LogStreamHandler(logging.StreamHandler): self.enabled = False def emit(self, record): - if not self.enabled: + if not self.enabled or self.stream is None: return try: msg = self.format(record) From 01bd1594f5bbabb21cfb85ee312064bbbe9c7a7b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:29:31 +0100 Subject: [PATCH 177/546] Remove condition that was moved up --- client/ayon_core/lib/log.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index d392610f32..692df93dad 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -45,8 +45,6 @@ class LogStreamHandler(logging.StreamHandler): msg = self.format(record) msg = Terminal.log(msg) stream = self.stream - if stream is None: - return stream.write(f"{msg}\n") self.flush() except (KeyboardInterrupt, SystemExit): From d4ace0706c90e032bdea5139cdc8e690b2f2c673 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:30:53 +0100 Subject: [PATCH 178/546] Remove unused import --- client/ayon_core/lib/path_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index efe2556afe..c6a833f43a 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -1,7 +1,6 @@ import os import re import logging -import platform import clique From df9821f928af8f26863f502737f863cd39778ef9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 20:31:21 +0100 Subject: [PATCH 179/546] Fix typo --- client/ayon_core/lib/path_tools.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index c6a833f43a..ebd22b43c5 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -185,7 +185,7 @@ def get_last_version_from_path(path_dir, filter): assert isinstance(filter, list) and ( len(filter) != 0), "`filter` argument needs to be list and not empty" - filtred_files = list() + filtered_files = list() # form regex for filtering pattern = r".*".join(filter) @@ -193,10 +193,10 @@ def get_last_version_from_path(path_dir, filter): for file in os.listdir(path_dir): if not re.findall(pattern, file): continue - filtred_files.append(file) + filtered_files.append(file) - if filtred_files: - sorted(filtred_files) - return filtred_files[-1] + if filtered_files: + sorted(filtered_files) + return filtered_files[-1] return None From a02954d908507538e62de6818d3339ee945ce4fe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 31 Oct 2024 23:19:23 +0100 Subject: [PATCH 180/546] Fix sorting Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/path_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index ebd22b43c5..31baac168c 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -196,7 +196,7 @@ def get_last_version_from_path(path_dir, filter): filtered_files.append(file) if filtered_files: - sorted(filtered_files) + filtered_files.sort() return filtered_files[-1] return None From 8c87a66cfe29c1eb8dc4a059364a38e3175aebf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 1 Nov 2024 12:45:13 +0100 Subject: [PATCH 181/546] Remove unused import --- client/ayon_core/lib/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/log.py b/client/ayon_core/lib/log.py index f48d3767c9..0c2fe5e2d4 100644 --- a/client/ayon_core/lib/log.py +++ b/client/ayon_core/lib/log.py @@ -1,6 +1,5 @@ import os import sys -import uuid import getpass import logging import platform From 1d053c6b7cee154b7e0fc19aa841428c632e60e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Nov 2024 15:40:04 +0100 Subject: [PATCH 182/546] action upload to ynput cloud --- .github/workflows/upload_to_ynput_cloud.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 .github/workflows/upload_to_ynput_cloud.yml diff --git a/.github/workflows/upload_to_ynput_cloud.yml b/.github/workflows/upload_to_ynput_cloud.yml new file mode 100644 index 0000000000..7745a8e016 --- /dev/null +++ b/.github/workflows/upload_to_ynput_cloud.yml @@ -0,0 +1,16 @@ +name: 📤 Upload to Ynput Cloud + +on: + workflow_dispatch: + release: + types: [published] + +jobs: + call-upload-to-ynput-cloud: + uses: ynput/ops-repo-automation/.github/workflows/upload_to_ynput_cloud.yml@main + secrets: + CI_EMAIL: ${{ secrets.CI_EMAIL }} + CI_USER: ${{ secrets.CI_USER }} + YNPUT_BOT_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }} + YNPUT_CLOUD_URL: ${{ secrets.YNPUT_CLOUD_URL }} + YNPUT_CLOUD_TOKEN: ${{ secrets.YNPUT_CLOUD_TOKEN }} From ead5d9963a63413f917ad21329979f7e1147de79 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Nov 2024 14:42:52 +0000 Subject: [PATCH 183/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index b2480af462..af93f3e660 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.5+dev" +__version__ = "1.0.6" diff --git a/package.py b/package.py index 38d930189f..b3bc01d0d0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.5+dev" +version = "1.0.6" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 31acb3f8b4..cccc81c069 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.5+dev" +version = "1.0.6" description = "" authors = ["Ynput Team "] readme = "README.md" From 348614745a8fc383d16bdf408f5eaa35bc124999 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Nov 2024 14:43:31 +0000 Subject: [PATCH 184/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index af93f3e660..2b2af81e18 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.6" +__version__ = "1.0.6+dev" diff --git a/package.py b/package.py index b3bc01d0d0..59f0e82be0 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.6" +version = "1.0.6+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index cccc81c069..ca626eff00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.6" +version = "1.0.6+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 7d2e676f522d956c406adc021ced1287004d1b1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:17:57 +0100 Subject: [PATCH 185/546] create attributes widget can show overriden values --- .../publisher/widgets/product_attributes.py | 67 ++++++++++++++++--- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 61d5ca111d..b49f846ffa 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -1,6 +1,9 @@ +from typing import Dict + from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.tools.utils import set_style_property from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend from ayon_core.tools.publisher.constants import ( @@ -9,6 +12,22 @@ from ayon_core.tools.publisher.constants import ( ) +def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): + set_style_property( + label, + "overriden", + "1" if overriden else "" + ) + + +class _CreateAttrDefInfo: + def __init__(self, attr_def, instance_ids, defaults, label_widget): + self.attr_def = attr_def + self.instance_ids = instance_ids + self.defaults = defaults + self.label_widget = label_widget + + class CreatorAttrsWidget(QtWidgets.QWidget): """Widget showing creator specific attributes for selected instances. @@ -51,8 +70,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} + self._attr_def_info_by_id: Dict[str, _CreateAttrDefInfo] = {} self._current_instance_ids = set() # To store content of scroll area to prevent garbage collection @@ -81,8 +99,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): prev_content_widget.deleteLater() self._content_widget = None - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} + self._attr_def_info_by_id = {} result = self._controller.get_creator_attribute_definitions( self._current_instance_ids @@ -97,9 +114,20 @@ class CreatorAttrsWidget(QtWidgets.QWidget): content_layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) row = 0 - for attr_def, instance_ids, values in result: + for attr_def, info_by_id in result: widget = create_widget_for_attr_def(attr_def, content_widget) + default_values = set() if attr_def.is_value_def: + default_values = [] + values = [] + for item in info_by_id.values(): + values.append(item["value"]) + # 'set' cannot be used for default values because they can + # be unhashable types, e.g. 'list'. + default = item["default"] + if default not in default_values: + default_values.append(default) + if len(values) == 1: value = values[0] if value is not None: @@ -108,8 +136,10 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widget.set_value(values, True) widget.value_changed.connect(self._input_value_changed) - self._attr_def_id_to_instances[attr_def.id] = instance_ids - self._attr_def_id_to_attr_def[attr_def.id] = attr_def + attr_def_info = _CreateAttrDefInfo( + attr_def, list(info_by_id), default_values, None + ) + self._attr_def_info_by_id[attr_def.id] = attr_def_info if not attr_def.visible: continue @@ -121,8 +151,14 @@ class CreatorAttrsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols label = None + is_overriden = False if attr_def.is_value_def: + is_overriden = any( + item["value"] != item["default"] + for item in info_by_id.values() + ) label = attr_def.label or attr_def.key + if label: label_widget = QtWidgets.QLabel(label, self) tooltip = attr_def.tooltip @@ -138,6 +174,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): ) if not attr_def.is_label_horizontal: row += 1 + attr_def_info.label_widget = label_widget + _set_label_overriden(label_widget, is_overriden) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -165,12 +203,19 @@ class CreatorAttrsWidget(QtWidgets.QWidget): break def _input_value_changed(self, value, attr_id): - instance_ids = self._attr_def_id_to_instances.get(attr_id) - attr_def = self._attr_def_id_to_attr_def.get(attr_id) - if not instance_ids or not attr_def: + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: return + + if attr_def_info.label_widget is not None: + defaults = attr_def_info.defaults + is_overriden = len(defaults) != 1 or value not in defaults + _set_label_overriden(attr_def_info.label_widget, is_overriden) + self._controller.set_instances_create_attr_values( - instance_ids, attr_def.key, value + attr_def_info.instance_ids, + attr_def_info.attr_def.key, + value ) From ecc7d2bde9d2755a74248c05a81482d2fe906c29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:18:15 +0100 Subject: [PATCH 186/546] change return type of 'get_creator_attribute_definitions' --- client/ayon_core/tools/publisher/abstract.py | 2 +- .../tools/publisher/models/create.py | 21 ++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index a6ae93cecd..5de3f72de1 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -366,7 +366,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): @abstractmethod def get_creator_attribute_definitions( self, instance_ids: Iterable[str] - ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: + ) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]: pass @abstractmethod diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 9c13d8ae2f..e4c208f1e8 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -769,7 +769,7 @@ class CreateModel: def get_creator_attribute_definitions( self, instance_ids: List[str] - ) -> List[Tuple[AbstractAttrDef, List[str], List[Any]]]: + ) -> List[Tuple[AbstractAttrDef, Dict[str, Dict[str, Any]]]]: """Collect creator attribute definitions for multuple instances. Args: @@ -796,12 +796,23 @@ class CreateModel: if found_idx is None: idx = len(output) - output.append((attr_def, [instance_id], [value])) + output.append(( + attr_def, + { + instance_id: { + "value": value, + "default": attr_def.default + } + } + )) _attr_defs[idx] = attr_def else: - _, ids, values = output[found_idx] - ids.append(instance_id) - values.append(value) + _, info_by_id = output[found_idx] + info_by_id[instance_id] = { + "value": value, + "default": attr_def.default + } + return output def set_instances_publish_attr_values( From 8a074daa2bb226de239d6d8291069c8796398086 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 4 Nov 2024 16:56:41 +0100 Subject: [PATCH 187/546] Fix single frame render --- client/ayon_core/pipeline/farm/pyblish_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 5908644dca..c70967dfc1 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -478,7 +478,8 @@ def _get_real_frames_to_render(frames): for frame in frames.split(","): if "-" in frame: splitted = frame.split("-") - frames_to_render.extend(range(int(splitted[0]), int(splitted[1]))) + frames_to_render.extend( + range(int(splitted[0]), int(splitted[1])+1)) else: frames_to_render.append(frame) return [str(frame_to_render) for frame_to_render in frames_to_render] From a1f74f2c783473fd2692efb7a9321dad36f87199 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:04:52 +0100 Subject: [PATCH 188/546] added styling to overriden label --- client/ayon_core/style/data.json | 4 ++++ client/ayon_core/style/style.css | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index 7389387d97..d4a8b6180b 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -60,7 +60,11 @@ "icon-alert-tools": "#AA5050", "icon-entity-default": "#bfccd6", "icon-entity-disabled": "#808080", + "font-entity-deprecated": "#666666", + + "font-overridden": "#FF8C1A", + "overlay-messages": { "close-btn": "#D3D8DE", "bg-success": "#458056", diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 3d84d917a4..0d1d4f710e 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -44,6 +44,10 @@ QLabel { background: transparent; } +QLabel[overriden="1"] { + color: {color:font-overridden}; +} + /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; From 0e0620770ff03039346e1eb29852531dbceed42c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 18:32:14 +0100 Subject: [PATCH 189/546] fix variable definition --- client/ayon_core/tools/publisher/widgets/product_attributes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index b49f846ffa..ab41812e4e 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -116,9 +116,8 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row = 0 for attr_def, info_by_id in result: widget = create_widget_for_attr_def(attr_def, content_widget) - default_values = set() + default_values = [] if attr_def.is_value_def: - default_values = [] values = [] for item in info_by_id.values(): values.append(item["value"]) From 9484c42b9a646690769c21b1794f354b49ee7fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:09:46 +0100 Subject: [PATCH 190/546] 'get_publish_attribute_definitions' returns default values too --- client/ayon_core/tools/publisher/abstract.py | 2 +- .../ayon_core/tools/publisher/models/create.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 5de3f72de1..7fad2b8176 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -383,7 +383,7 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[str, Any]]] + Dict[str, List[Tuple[str, Any, Any]]] ]]: pass diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index e4c208f1e8..8763d79712 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -846,7 +846,7 @@ class CreateModel: ) -> List[Tuple[ str, List[AbstractAttrDef], - Dict[str, List[Tuple[str, Any]]] + Dict[str, List[Tuple[str, Any, Any]]] ]]: """Collect publish attribute definitions for passed instances. @@ -876,21 +876,21 @@ class CreateModel: attr_defs = attr_val.attr_defs if not attr_defs: continue + plugin_attr_defs = all_defs_by_plugin_name.setdefault( plugin_name, [] ) - plugin_attr_defs.append(attr_defs) - plugin_values = all_plugin_values.setdefault(plugin_name, {}) + plugin_attr_defs.append(attr_defs) + for attr_def in attr_defs: if isinstance(attr_def, UIDef): continue - attr_values = plugin_values.setdefault(attr_def.key, []) - - value = attr_val[attr_def.key] - attr_values.append((item_id, value)) + attr_values.append( + (item_id, attr_val[attr_def.key], attr_def.default) + ) attr_defs_by_plugin_name = {} for plugin_name, attr_defs in all_defs_by_plugin_name.items(): @@ -904,7 +904,7 @@ class CreateModel: output.append(( plugin_name, attr_defs_by_plugin_name[plugin_name], - all_plugin_values + all_plugin_values[plugin_name], )) return output From d99dff1610f4243f8217a35968d73c54e7a24804 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:10:01 +0100 Subject: [PATCH 191/546] modified publish attributes to display overrides --- .../publisher/widgets/product_attributes.py | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index ab41812e4e..206eebc6f1 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -1,8 +1,9 @@ -from typing import Dict +import typing +from typing import Dict, List, Any from qtpy import QtWidgets, QtCore -from ayon_core.lib.attribute_definitions import UnknownDef +from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef from ayon_core.tools.utils import set_style_property from ayon_core.tools.attribute_defs import create_widget_for_attr_def from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend @@ -11,6 +12,9 @@ from ayon_core.tools.publisher.constants import ( INPUTS_LAYOUT_VSPACING, ) +if typing.TYPE_CHECKING: + from typing import Union + def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): set_style_property( @@ -28,6 +32,22 @@ class _CreateAttrDefInfo: self.label_widget = label_widget +class _PublishAttrDefInfo: + def __init__( + self, + attr_def: AbstractAttrDef, + plugin_name: str, + instance_ids: List["Union[str, None]"], + defaults: List[Any], + label_widget: "Union[None, QtWidgets.QLabel]", + ): + self.attr_def = attr_def + self.plugin_name = plugin_name + self.instance_ids = instance_ids + self.defaults = defaults + self.label_widget = label_widget + + class CreatorAttrsWidget(QtWidgets.QWidget): """Widget showing creator specific attributes for selected instances. @@ -267,9 +287,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._controller: AbstractPublisherFrontend = controller self._scroll_area = scroll_area - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._attr_def_id_to_plugin_name = {} + self._attr_def_info_by_id: Dict[str, _PublishAttrDefInfo] = {} # Store content of scroll area to prevent garbage collection self._content_widget = None @@ -298,9 +316,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): self._content_widget = None - self._attr_def_id_to_instances = {} - self._attr_def_id_to_attr_def = {} - self._attr_def_id_to_plugin_name = {} + self._attr_def_info_by_id = {} result = self._controller.get_publish_attribute_definitions( self._current_instance_ids, self._context_selected @@ -319,9 +335,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): content_layout.addStretch(1) row = 0 - for plugin_name, attr_defs, all_plugin_values in result: - plugin_values = all_plugin_values[plugin_name] - + for plugin_name, attr_defs, plugin_values in result: for attr_def in attr_defs: widget = create_widget_for_attr_def( attr_def, content_widget @@ -334,6 +348,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.setVisible(False) visible_widget = False + label_widget = None if visible_widget: expand_cols = 2 if attr_def.is_value_def and attr_def.is_label_horizontal: @@ -368,35 +383,58 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.value_changed.connect(self._input_value_changed) - attr_values = plugin_values[attr_def.key] - multivalue = len(attr_values) > 1 + instance_ids = [] values = [] - instances = [] - for instance, value in attr_values: + default_values = [] + is_overriden = False + for (instance_id, value, default_value) in ( + plugin_values.get(attr_def.key, []) + ): + instance_ids.append(instance_id) values.append(value) - instances.append(instance) + if not is_overriden and value != default_value: + is_overriden = True + # 'set' cannot be used for default values because they can + # be unhashable types, e.g. 'list'. + if default_value not in default_values: + default_values.append(default_value) - self._attr_def_id_to_attr_def[attr_def.id] = attr_def - self._attr_def_id_to_instances[attr_def.id] = instances - self._attr_def_id_to_plugin_name[attr_def.id] = plugin_name + multivalue = len(values) > 1 + + self._attr_def_info_by_id[attr_def.id] = _PublishAttrDefInfo( + attr_def, + plugin_name, + instance_ids, + default_values, + label_widget, + ) if multivalue: widget.set_value(values, multivalue) else: widget.set_value(values[0]) + if label_widget is not None: + _set_label_overriden(label_widget, is_overriden) + self._scroll_area.setWidget(content_widget) self._content_widget = content_widget def _input_value_changed(self, value, attr_id): - instance_ids = self._attr_def_id_to_instances.get(attr_id) - attr_def = self._attr_def_id_to_attr_def.get(attr_id) - plugin_name = self._attr_def_id_to_plugin_name.get(attr_id) - if not instance_ids or not attr_def or not plugin_name: + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: return + if attr_def_info.label_widget is not None: + defaults = attr_def_info.defaults + is_overriden = len(defaults) != 1 or value not in defaults + _set_label_overriden(attr_def_info.label_widget, is_overriden) + self._controller.set_instances_publish_attr_values( - instance_ids, plugin_name, attr_def.key, value + attr_def_info.instance_ids, + attr_def_info.plugin_name, + attr_def_info.attr_def.key, + value ) def _on_instance_attr_defs_change(self, event): From 98674eb4366bb063d9d09789223b3403db74f7bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:07:40 +0100 Subject: [PATCH 192/546] change overriden color --- client/ayon_core/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index d4a8b6180b..748a51238a 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -63,7 +63,7 @@ "font-entity-deprecated": "#666666", - "font-overridden": "#FF8C1A", + "font-overridden": "#33B461", "overlay-messages": { "close-btn": "#D3D8DE", From 46acbacaac83de7245404e978fe9d9423aa5b36a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 10:51:29 +0100 Subject: [PATCH 193/546] added typehints and tiny docstring --- .../publisher/widgets/product_attributes.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 206eebc6f1..b0b2640640 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -25,14 +25,22 @@ def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): class _CreateAttrDefInfo: - def __init__(self, attr_def, instance_ids, defaults, label_widget): - self.attr_def = attr_def - self.instance_ids = instance_ids - self.defaults = defaults - self.label_widget = label_widget + """Helper class to store information about create attribute definition.""" + def __init__( + self, + attr_def: AbstractAttrDef, + instance_ids: List["Union[str, None]"], + defaults: List[Any], + label_widget: "Union[None, QtWidgets.QLabel]", + ): + self.attr_def: AbstractAttrDef = attr_def + self.instance_ids: List["Union[str, None]"] = instance_ids + self.defaults: List[Any] = defaults + self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget class _PublishAttrDefInfo: + """Helper class to store information about publish attribute definition.""" def __init__( self, attr_def: AbstractAttrDef, @@ -41,11 +49,11 @@ class _PublishAttrDefInfo: defaults: List[Any], label_widget: "Union[None, QtWidgets.QLabel]", ): - self.attr_def = attr_def - self.plugin_name = plugin_name - self.instance_ids = instance_ids - self.defaults = defaults - self.label_widget = label_widget + self.attr_def: AbstractAttrDef = attr_def + self.plugin_name: str = plugin_name + self.instance_ids: List["Union[str, None]"] = instance_ids + self.defaults: List[Any] = defaults + self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget class CreatorAttrsWidget(QtWidgets.QWidget): From 994dc956c264daccffb5b8bbe7aa8786589be2d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:16:57 +0100 Subject: [PATCH 194/546] use blue color --- client/ayon_core/style/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/data.json b/client/ayon_core/style/data.json index 748a51238a..24629ec085 100644 --- a/client/ayon_core/style/data.json +++ b/client/ayon_core/style/data.json @@ -63,7 +63,7 @@ "font-entity-deprecated": "#666666", - "font-overridden": "#33B461", + "font-overridden": "#91CDFC", "overlay-messages": { "close-btn": "#D3D8DE", From 3a8a9ec4831be2b72d6ae3f7a2ba35fc20a32482 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:25:20 +0100 Subject: [PATCH 195/546] implemented logic to revert to default values --- client/ayon_core/tools/publisher/abstract.py | 17 ++++ client/ayon_core/tools/publisher/control.py | 12 +++ .../tools/publisher/models/create.py | 98 ++++++++++++------- 3 files changed, 93 insertions(+), 34 deletions(-) diff --git a/client/ayon_core/tools/publisher/abstract.py b/client/ayon_core/tools/publisher/abstract.py index 7fad2b8176..4ed91813d3 100644 --- a/client/ayon_core/tools/publisher/abstract.py +++ b/client/ayon_core/tools/publisher/abstract.py @@ -375,6 +375,14 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def revert_instances_create_attr_values( + self, + instance_ids: List["Union[str, None]"], + key: str, + ): + pass + @abstractmethod def get_publish_attribute_definitions( self, @@ -397,6 +405,15 @@ class AbstractPublisherFrontend(AbstractPublisherCommon): ): pass + @abstractmethod + def revert_instances_publish_attr_values( + self, + instance_ids: List["Union[str, None]"], + plugin_name: str, + key: str, + ): + pass + @abstractmethod def get_product_name( self, diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 347755d557..98fdda08cf 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -412,6 +412,11 @@ class PublisherController( instance_ids, key, value ) + def revert_instances_create_attr_values(self, instance_ids, key): + self._create_model.revert_instances_create_attr_values( + instance_ids, key + ) + def get_publish_attribute_definitions(self, instance_ids, include_context): """Collect publish attribute definitions for passed instances. @@ -432,6 +437,13 @@ class PublisherController( instance_ids, plugin_name, key, value ) + def revert_instances_publish_attr_values( + self, instance_ids, plugin_name, key + ): + return self._create_model.revert_instances_publish_attr_values( + instance_ids, plugin_name, key + ) + def get_product_name( self, creator_identifier, diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 8763d79712..ca26749b65 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -40,6 +40,7 @@ from ayon_core.tools.publisher.abstract import ( ) CREATE_EVENT_SOURCE = "publisher.create.model" +_DEFAULT_VALUE = object() class CreatorType: @@ -752,20 +753,12 @@ class CreateModel: self._remove_instances_from_context(instance_ids) def set_instances_create_attr_values(self, instance_ids, key, value): - with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): - for instance_id in instance_ids: - instance = self._get_instance_by_id(instance_id) - creator_attributes = instance["creator_attributes"] - attr_def = creator_attributes.get_attr_def(key) - if ( - attr_def is None - or not attr_def.is_value_def - or not attr_def.visible - or not attr_def.enabled - or not attr_def.is_value_valid(value) - ): - continue - creator_attributes[key] = value + self._set_instances_create_attr_values(instance_ids, key, value) + + def revert_instances_create_attr_values(self, instance_ids, key): + self._set_instances_create_attr_values( + instance_ids, key, _DEFAULT_VALUE + ) def get_creator_attribute_definitions( self, instance_ids: List[str] @@ -816,28 +809,18 @@ class CreateModel: return output def set_instances_publish_attr_values( - self, instance_ids, plugin_name, key, value + self, instance_ids, plugin_name, key, value ): - with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): - for instance_id in instance_ids: - if instance_id is None: - instance = self._create_context - else: - instance = self._get_instance_by_id(instance_id) - plugin_val = instance.publish_attributes[plugin_name] - attr_def = plugin_val.get_attr_def(key) - # Ignore if attribute is not available or enabled/visible - # on the instance, or the value is not valid for definition - if ( - attr_def is None - or not attr_def.is_value_def - or not attr_def.visible - or not attr_def.enabled - or not attr_def.is_value_valid(value) - ): - continue + self._set_instances_publish_attr_values( + instance_ids, plugin_name, key, value + ) - plugin_val[key] = value + def revert_instances_publish_attr_values( + self, instance_ids, plugin_name, key + ): + self._set_instances_publish_attr_values( + instance_ids, plugin_name, key, _DEFAULT_VALUE + ) def get_publish_attribute_definitions( self, @@ -1064,6 +1047,53 @@ class CreateModel: CreatorItem.from_creator(creator) ) + def _set_instances_create_attr_values(self, instance_ids, key, value): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id in instance_ids: + instance = self._get_instance_by_id(instance_id) + creator_attributes = instance["creator_attributes"] + attr_def = creator_attributes.get_attr_def(key) + if ( + attr_def is None + or not attr_def.is_value_def + or not attr_def.visible + or not attr_def.enabled + ): + continue + + if value is _DEFAULT_VALUE: + creator_attributes[key] = attr_def.default + + elif attr_def.is_value_valid(value): + creator_attributes[key] = value + + def _set_instances_publish_attr_values( + self, instance_ids, plugin_name, key, value + ): + with self._create_context.bulk_value_changes(CREATE_EVENT_SOURCE): + for instance_id in instance_ids: + if instance_id is None: + instance = self._create_context + else: + instance = self._get_instance_by_id(instance_id) + plugin_val = instance.publish_attributes[plugin_name] + attr_def = plugin_val.get_attr_def(key) + # Ignore if attribute is not available or enabled/visible + # on the instance, or the value is not valid for definition + if ( + attr_def is None + or not attr_def.is_value_def + or not attr_def.visible + or not attr_def.enabled + ): + continue + + if value is _DEFAULT_VALUE: + plugin_val[key] = attr_def.default + + elif attr_def.is_value_valid(value): + plugin_val[key] = value + def _cc_added_instance(self, event): instance_ids = { instance.id From 56a07fe9183eaa33ad202261afb2d880499d2805 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:28:30 +0100 Subject: [PATCH 196/546] added 'AttributeDefinitionsLabel' helper label widget --- .../ayon_core/tools/attribute_defs/widgets.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 026aea00ad..e1977cca2c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -20,11 +20,14 @@ from ayon_core.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, + set_style_property, ) from ayon_core.tools.utils import NiceCheckbox from .files_widget import FilesWidget +_REVERT_TO_DEFAULT_LABEL = "Revert to default" + def create_widget_for_attr_def(attr_def, parent=None): widget = _create_widget_for_attr_def(attr_def, parent) @@ -74,6 +77,52 @@ def _create_widget_for_attr_def(attr_def, parent=None): )) +class AttributeDefinitionsLabel(QtWidgets.QLabel): + """Label related to value attribute definition. + + Label is used to show attribute definition label and to show if value + is overridden. + + Label can be right-clicked to revert value to default. + """ + revert_to_default_requested = QtCore.Signal(str) + + def __init__( + self, + attr_id: str, + label: str, + parent: QtWidgets.QWidget, + ): + super().__init__(label, parent) + + self._attr_id = attr_id + self._overridden = False + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + self.customContextMenuRequested.connect(self._on_context_menu) + + def set_overridden(self, overridden: bool): + if self._overridden == overridden: + return + self._overridden = overridden + set_style_property( + self, + "overridden", + "1" if overridden else "" + ) + + def _on_context_menu(self, point: QtCore.QPoint): + menu = QtWidgets.QMenu(self) + action = QtWidgets.QAction(menu) + action.setText(_REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self._request_revert_to_default) + menu.addAction(action) + menu.exec_(self.mapToGlobal(point)) + + def _request_revert_to_default(self): + self.revert_to_default_requested.emit(self._attr_id) + + class AttributeDefinitionsWidget(QtWidgets.QWidget): """Create widgets for attribute definitions in grid layout. From 33a5195b7156e444139232b55545058f34e173bc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:28:46 +0100 Subject: [PATCH 197/546] added 'AttributeDefinitionsLabel' to init --- client/ayon_core/tools/attribute_defs/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/attribute_defs/__init__.py b/client/ayon_core/tools/attribute_defs/__init__.py index f991fdec3d..7f6cbb41be 100644 --- a/client/ayon_core/tools/attribute_defs/__init__.py +++ b/client/ayon_core/tools/attribute_defs/__init__.py @@ -1,6 +1,7 @@ from .widgets import ( create_widget_for_attr_def, AttributeDefinitionsWidget, + AttributeDefinitionsLabel, ) from .dialog import ( @@ -11,6 +12,7 @@ from .dialog import ( __all__ = ( "create_widget_for_attr_def", "AttributeDefinitionsWidget", + "AttributeDefinitionsLabel", "AttributeDefinitionsDialog", ) From 7d0f1f97e4ded1cc2a855e7db51fcceb2affc560 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:29:48 +0100 Subject: [PATCH 198/546] use new label in product attributes --- .../publisher/widgets/product_attributes.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index b0b2640640..07cbfb1907 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -4,8 +4,10 @@ from typing import Dict, List, Any from qtpy import QtWidgets, QtCore from ayon_core.lib.attribute_definitions import AbstractAttrDef, UnknownDef -from ayon_core.tools.utils import set_style_property -from ayon_core.tools.attribute_defs import create_widget_for_attr_def +from ayon_core.tools.attribute_defs import ( + create_widget_for_attr_def, + AttributeDefinitionsLabel, +) from ayon_core.tools.publisher.abstract import AbstractPublisherFrontend from ayon_core.tools.publisher.constants import ( INPUTS_LAYOUT_HSPACING, @@ -16,14 +18,6 @@ if typing.TYPE_CHECKING: from typing import Union -def _set_label_overriden(label: QtWidgets.QLabel, overriden: bool): - set_style_property( - label, - "overriden", - "1" if overriden else "" - ) - - class _CreateAttrDefInfo: """Helper class to store information about create attribute definition.""" def __init__( @@ -31,12 +25,14 @@ class _CreateAttrDefInfo: attr_def: AbstractAttrDef, instance_ids: List["Union[str, None]"], defaults: List[Any], - label_widget: "Union[None, QtWidgets.QLabel]", + label_widget: "Union[AttributeDefinitionsLabel, None]", ): self.attr_def: AbstractAttrDef = attr_def self.instance_ids: List["Union[str, None]"] = instance_ids self.defaults: List[Any] = defaults - self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget + self.label_widget: "Union[AttributeDefinitionsLabel, None]" = ( + label_widget + ) class _PublishAttrDefInfo: @@ -47,13 +43,15 @@ class _PublishAttrDefInfo: plugin_name: str, instance_ids: List["Union[str, None]"], defaults: List[Any], - label_widget: "Union[None, QtWidgets.QLabel]", + label_widget: "Union[AttributeDefinitionsLabel, None]", ): self.attr_def: AbstractAttrDef = attr_def self.plugin_name: str = plugin_name self.instance_ids: List["Union[str, None]"] = instance_ids self.defaults: List[Any] = defaults - self.label_widget: "Union[None, QtWidgets.QLabel]" = label_widget + self.label_widget: "Union[AttributeDefinitionsLabel, None]" = ( + label_widget + ) class CreatorAttrsWidget(QtWidgets.QWidget): @@ -187,7 +185,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): label = attr_def.label or attr_def.key if label: - label_widget = QtWidgets.QLabel(label, self) + label_widget = AttributeDefinitionsLabel( + attr_def.id, label, self + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -202,7 +202,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): if not attr_def.is_label_horizontal: row += 1 attr_def_info.label_widget = label_widget - _set_label_overriden(label_widget, is_overriden) + label_widget.set_overridden(is_overriden) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -237,7 +237,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): if attr_def_info.label_widget is not None: defaults = attr_def_info.defaults is_overriden = len(defaults) != 1 or value not in defaults - _set_label_overriden(attr_def_info.label_widget, is_overriden) + attr_def_info.label_widget.set_overridden(is_overriden) self._controller.set_instances_create_attr_values( attr_def_info.instance_ids, @@ -367,7 +367,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): if attr_def.is_value_def: label = attr_def.label or attr_def.key if label: - label_widget = QtWidgets.QLabel(label, content_widget) + label_widget = AttributeDefinitionsLabel( + attr_def.id, label, content_widget + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -423,7 +425,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): widget.set_value(values[0]) if label_widget is not None: - _set_label_overriden(label_widget, is_overriden) + label_widget.set_overridden(is_overriden) self._scroll_area.setWidget(content_widget) self._content_widget = content_widget @@ -436,7 +438,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): if attr_def_info.label_widget is not None: defaults = attr_def_info.defaults is_overriden = len(defaults) != 1 or value not in defaults - _set_label_overriden(attr_def_info.label_widget, is_overriden) + attr_def_info.label_widget.set_overridden(is_overriden) self._controller.set_instances_publish_attr_values( attr_def_info.instance_ids, From 5569c95aefb02dec9ef1760fa6dd7a0eb497707b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:29:56 +0100 Subject: [PATCH 199/546] change style of label --- client/ayon_core/style/style.css | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index 0d1d4f710e..bd96a3aeed 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -44,10 +44,6 @@ QLabel { background: transparent; } -QLabel[overriden="1"] { - color: {color:font-overridden}; -} - /* Inputs */ QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { border: 1px solid {color:border}; @@ -1589,6 +1585,10 @@ CreateNextPageOverlay { } /* Attribute Definition widgets */ +AttributeDefinitionsLabel[overridden="1"] { + color: {color:font-overridden}; +} + AttributeDefinitionsWidget QAbstractSpinBox, QLineEdit, QPlainTextEdit, QTextEdit { padding: 1px; } From 9de6a74789a4d8ba92b66806b1005e37e3a977b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:31:23 +0100 Subject: [PATCH 200/546] base attribute widget can handle reset to default logic --- .../ayon_core/tools/attribute_defs/widgets.py | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index e1977cca2c..09637a9696 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -241,11 +241,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): class _BaseAttrDefWidget(QtWidgets.QWidget): # Type 'object' may not work with older PySide versions value_changed = QtCore.Signal(object, str) + revert_to_default_requested = QtCore.Signal(str) - def __init__(self, attr_def, parent): - super(_BaseAttrDefWidget, self).__init__(parent) + def __init__( + self, + attr_def: AbstractAttrDef, + parent: "Union[QtWidgets.QWidget, None]", + handle_revert_to_default: Optional[bool] = True, + ): + super().__init__(parent) - self.attr_def = attr_def + self.attr_def: AbstractAttrDef = attr_def + self._handle_revert_to_default: bool = handle_revert_to_default main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -254,6 +261,15 @@ class _BaseAttrDefWidget(QtWidgets.QWidget): self._ui_init() + def revert_to_default_value(self): + if not self.attr_def.is_value_def: + return + + if self._handle_revert_to_default: + self.set_value(self.attr_def.default) + else: + self.revert_to_default_requested.emit(self.attr_def.id) + def _ui_init(self): raise NotImplementedError( "Method '_ui_init' is not implemented. {}".format( From bbe1d9e3fd1b8e6ca284614ccf5e6ee5d212efd5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:32:19 +0100 Subject: [PATCH 201/546] 'create_widget_for_attr_def' can pass in all init args --- .../ayon_core/tools/attribute_defs/widgets.py | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 09637a9696..d3f51a196c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -1,4 +1,6 @@ import copy +import typing +from typing import Optional from qtpy import QtWidgets, QtCore @@ -26,11 +28,20 @@ from ayon_core.tools.utils import NiceCheckbox from .files_widget import FilesWidget +if typing.TYPE_CHECKING: + from typing import Union + _REVERT_TO_DEFAULT_LABEL = "Revert to default" -def create_widget_for_attr_def(attr_def, parent=None): - widget = _create_widget_for_attr_def(attr_def, parent) +def create_widget_for_attr_def( + attr_def: AbstractAttrDef, + parent: Optional[QtWidgets.QWidget] = None, + handle_revert_to_default: Optional[bool] = True, +): + widget = _create_widget_for_attr_def( + attr_def, parent, handle_revert_to_default + ) if not attr_def.visible: widget.setVisible(False) @@ -39,42 +50,50 @@ def create_widget_for_attr_def(attr_def, parent=None): return widget -def _create_widget_for_attr_def(attr_def, parent=None): +def _create_widget_for_attr_def( + attr_def: AbstractAttrDef, + parent: "Union[QtWidgets.QWidget, None]", + handle_revert_to_default: bool, +): if not isinstance(attr_def, AbstractAttrDef): raise TypeError("Unexpected type \"{}\" expected \"{}\"".format( str(type(attr_def)), AbstractAttrDef )) + cls = None if isinstance(attr_def, NumberDef): - return NumberAttrWidget(attr_def, parent) + cls = NumberAttrWidget - if isinstance(attr_def, TextDef): - return TextAttrWidget(attr_def, parent) + elif isinstance(attr_def, TextDef): + cls = TextAttrWidget - if isinstance(attr_def, EnumDef): - return EnumAttrWidget(attr_def, parent) + elif isinstance(attr_def, EnumDef): + cls = EnumAttrWidget - if isinstance(attr_def, BoolDef): - return BoolAttrWidget(attr_def, parent) + elif isinstance(attr_def, BoolDef): + cls = BoolAttrWidget - if isinstance(attr_def, UnknownDef): - return UnknownAttrWidget(attr_def, parent) + elif isinstance(attr_def, UnknownDef): + cls = UnknownAttrWidget - if isinstance(attr_def, HiddenDef): - return HiddenAttrWidget(attr_def, parent) + elif isinstance(attr_def, HiddenDef): + cls = HiddenAttrWidget - if isinstance(attr_def, FileDef): - return FileAttrWidget(attr_def, parent) + elif isinstance(attr_def, FileDef): + cls = FileAttrWidget - if isinstance(attr_def, UISeparatorDef): - return SeparatorAttrWidget(attr_def, parent) + elif isinstance(attr_def, UISeparatorDef): + cls = SeparatorAttrWidget - if isinstance(attr_def, UILabelDef): - return LabelAttrWidget(attr_def, parent) + elif isinstance(attr_def, UILabelDef): + cls = LabelAttrWidget - raise ValueError("Unknown attribute definition \"{}\"".format( - str(type(attr_def)) - )) + if cls is None: + raise ValueError("Unknown attribute definition \"{}\"".format( + str(type(attr_def)) + )) + + return cls(attr_def, parent, handle_revert_to_default) class AttributeDefinitionsLabel(QtWidgets.QLabel): From c23cf6746d8014f46921fcc610d894b36f027d43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:32:58 +0100 Subject: [PATCH 202/546] 'AttributeDefinitionsWidget' shows overriden values on labels --- .../ayon_core/tools/attribute_defs/widgets.py | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index d3f51a196c..94121e51bc 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -151,16 +151,18 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): """ def __init__(self, attr_defs=None, parent=None): - super(AttributeDefinitionsWidget, self).__init__(parent) + super().__init__(parent) - self._widgets = [] + self._widgets_by_id = {} + self._labels_by_id = {} self._current_keys = set() self.set_attr_defs(attr_defs) def clear_attr_defs(self): """Remove all existing widgets and reset layout if needed.""" - self._widgets = [] + self._widgets_by_id = {} + self._labels_by_id = {} self._current_keys = set() layout = self.layout() @@ -202,6 +204,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): self._current_keys.add(attr_def.key) widget = create_widget_for_attr_def(attr_def, self) self._widgets.append(widget) + self._widgets_by_id[attr_def.id] = widget if not attr_def.visible: continue @@ -213,7 +216,13 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): col_num = 2 - expand_cols if attr_def.is_value_def and attr_def.label: - label_widget = QtWidgets.QLabel(attr_def.label, self) + label_widget = AttributeDefinitionsLabel( + attr_def.id, attr_def.label, self + ) + label_widget.revert_to_default_requested.connect( + self._on_revert_request + ) + self._labels_by_id[attr_def.id] = label_widget tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -228,6 +237,9 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): if not attr_def.is_label_horizontal: row += 1 + if attr_def.is_value_def: + widget.value_changed.connect(self._on_value_change) + layout.addWidget( widget, row, col_num, 1, expand_cols ) @@ -236,7 +248,7 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): def set_value(self, value): new_value = copy.deepcopy(value) unused_keys = set(new_value.keys()) - for widget in self._widgets: + for widget in self._widgets_by_id.values(): attr_def = widget.attr_def if attr_def.key not in new_value: continue @@ -249,13 +261,26 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): def current_value(self): output = {} - for widget in self._widgets: + for widget in self._widgets_by_id.values(): attr_def = widget.attr_def if not isinstance(attr_def, UIDef): output[attr_def.key] = widget.current_value() return output + def _on_revert_request(self, attr_id): + widget = self._widgets_by_id.get(attr_id) + if widget is not None: + widget.set_value(widget.attr_def.default) + + def _on_value_change(self, value, attr_id): + widget = self._widgets_by_id.get(attr_id) + if widget is None: + return + label = self._labels_by_id.get(attr_id) + if label is not None: + label.set_overridden(value != widget.attr_def.default) + class _BaseAttrDefWidget(QtWidgets.QWidget): # Type 'object' may not work with older PySide versions From 47973464fdd7fd923a208d8b44b78da15bcd69f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:33:38 +0100 Subject: [PATCH 203/546] remoe python 2 super calls --- client/ayon_core/tools/attribute_defs/widgets.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 94121e51bc..118f4b5f64 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -364,7 +364,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): clicked = QtCore.Signal() def __init__(self, text, parent): - super(ClickableLineEdit, self).__init__(parent) + super().__init__(parent) self.setText(text) self.setReadOnly(True) @@ -373,7 +373,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): def mousePressEvent(self, event): if event.button() == QtCore.Qt.LeftButton: self._mouse_pressed = True - super(ClickableLineEdit, self).mousePressEvent(event) + super().mousePressEvent(event) def mouseReleaseEvent(self, event): if self._mouse_pressed: @@ -381,7 +381,7 @@ class ClickableLineEdit(QtWidgets.QLineEdit): if self.rect().contains(event.pos()): self.clicked.emit() - super(ClickableLineEdit, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) class NumberAttrWidget(_BaseAttrDefWidget): @@ -596,7 +596,7 @@ class BoolAttrWidget(_BaseAttrDefWidget): class EnumAttrWidget(_BaseAttrDefWidget): def __init__(self, *args, **kwargs): self._multivalue = False - super(EnumAttrWidget, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) @property def multiselection(self): @@ -723,7 +723,7 @@ class HiddenAttrWidget(_BaseAttrDefWidget): def setVisible(self, visible): if visible: visible = False - super(HiddenAttrWidget, self).setVisible(visible) + super().setVisible(visible) def current_value(self): if self._multivalue: From b5d018c071341474e91c97f60b389a21e45f30b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:34:41 +0100 Subject: [PATCH 204/546] publisher does handle revert to default --- .../publisher/widgets/product_attributes.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 07cbfb1907..cb165d1be7 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -141,7 +141,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row = 0 for attr_def, info_by_id in result: - widget = create_widget_for_attr_def(attr_def, content_widget) + widget = create_widget_for_attr_def( + attr_def, content_widget, handle_revert_to_default=False + ) default_values = [] if attr_def.is_value_def: values = [] @@ -161,6 +163,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): widget.set_value(values, True) widget.value_changed.connect(self._input_value_changed) + widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) attr_def_info = _CreateAttrDefInfo( attr_def, list(info_by_id), default_values, None ) @@ -203,6 +208,9 @@ class CreatorAttrsWidget(QtWidgets.QWidget): row += 1 attr_def_info.label_widget = label_widget label_widget.set_overridden(is_overriden) + label_widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) content_layout.addWidget( widget, row, col_num, 1, expand_cols @@ -245,6 +253,15 @@ class CreatorAttrsWidget(QtWidgets.QWidget): value ) + def _on_request_revert_to_default(self, attr_id): + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: + return + self._controller.revert_instances_create_attr_values( + attr_def_info.instance_ids, + attr_def_info.attr_def.key, + ) + class PublishPluginAttrsWidget(QtWidgets.QWidget): """Widget showing publish plugin attributes for selected instances. @@ -346,7 +363,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): for plugin_name, attr_defs, plugin_values in result: for attr_def in attr_defs: widget = create_widget_for_attr_def( - attr_def, content_widget + attr_def, content_widget, handle_revert_to_default=False ) visible_widget = attr_def.visible # Hide unknown values of publish plugins @@ -370,6 +387,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): label_widget = AttributeDefinitionsLabel( attr_def.id, label, content_widget ) + label_widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) tooltip = attr_def.tooltip if tooltip: label_widget.setToolTip(tooltip) @@ -392,6 +412,9 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): continue widget.value_changed.connect(self._input_value_changed) + widget.revert_to_default_requested.connect( + self._on_request_revert_to_default + ) instance_ids = [] values = [] @@ -447,6 +470,17 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): value ) + def _on_request_revert_to_default(self, attr_id): + attr_def_info = self._attr_def_info_by_id.get(attr_id) + if attr_def_info is None: + return + + self._controller.revert_instances_publish_attr_values( + attr_def_info.instance_ids, + attr_def_info.plugin_name, + attr_def_info.attr_def.key, + ) + def _on_instance_attr_defs_change(self, event): for instance_id in event.data: if ( From 2d51436da71fb9b6b95409779bc1f33715e837af Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:46:35 +0100 Subject: [PATCH 205/546] refresh content --- client/ayon_core/tools/publisher/widgets/product_attributes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index cb165d1be7..3ff295c986 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -261,6 +261,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): attr_def_info.instance_ids, attr_def_info.attr_def.key, ) + self._refresh_content() class PublishPluginAttrsWidget(QtWidgets.QWidget): @@ -480,6 +481,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): attr_def_info.plugin_name, attr_def_info.attr_def.key, ) + self._refresh_content() def _on_instance_attr_defs_change(self, event): for instance_id in event.data: From 9be42980bdb46578b6a04a7424d1a04b165e507e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:58:21 +0100 Subject: [PATCH 206/546] implemented request restart logic for most of widgets --- .../tools/attribute_defs/_constants.py | 1 + .../ayon_core/tools/attribute_defs/widgets.py | 58 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 client/ayon_core/tools/attribute_defs/_constants.py diff --git a/client/ayon_core/tools/attribute_defs/_constants.py b/client/ayon_core/tools/attribute_defs/_constants.py new file mode 100644 index 0000000000..b58a05bac6 --- /dev/null +++ b/client/ayon_core/tools/attribute_defs/_constants.py @@ -0,0 +1 @@ +REVERT_TO_DEFAULT_LABEL = "Revert to default" diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 118f4b5f64..03482c1006 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -26,13 +26,12 @@ from ayon_core.tools.utils import ( ) from ayon_core.tools.utils import NiceCheckbox +from ._constants import REVERT_TO_DEFAULT_LABEL from .files_widget import FilesWidget if typing.TYPE_CHECKING: from typing import Union -_REVERT_TO_DEFAULT_LABEL = "Revert to default" - def create_widget_for_attr_def( attr_def: AbstractAttrDef, @@ -133,7 +132,7 @@ class AttributeDefinitionsLabel(QtWidgets.QLabel): def _on_context_menu(self, point: QtCore.QPoint): menu = QtWidgets.QMenu(self) action = QtWidgets.QAction(menu) - action.setText(_REVERT_TO_DEFAULT_LABEL) + action.setText(REVERT_TO_DEFAULT_LABEL) action.triggered.connect(self._request_revert_to_default) menu.addAction(action) menu.exec_(self.mapToGlobal(point)) @@ -393,6 +392,9 @@ class NumberAttrWidget(_BaseAttrDefWidget): else: input_widget = FocusSpinBox(self) + # Override context menu event to add revert to default action + input_widget.contextMenuEvent = self._input_widget_context_event + if self.attr_def.tooltip: input_widget.setToolTip(self.attr_def.tooltip) @@ -430,6 +432,16 @@ class NumberAttrWidget(_BaseAttrDefWidget): self._set_multiselection_visible(True) return False + def _input_widget_context_event(self, event): + line_edit = self._input_widget.lineEdit() + menu = line_edit.createStandardContextMenu() + menu.setAttribute(QtCore.Qt.WA_DeleteOnClose) + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + menu.popup(event.globalPos()) + def current_value(self): return self._input_widget.value() @@ -495,6 +507,9 @@ class TextAttrWidget(_BaseAttrDefWidget): else: input_widget = QtWidgets.QLineEdit(self) + # Override context menu event to add revert to default action + input_widget.contextMenuEvent = self._input_widget_context_event + if ( self.attr_def.placeholder and hasattr(input_widget, "setPlaceholderText") @@ -516,6 +531,15 @@ class TextAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) + def _input_widget_context_event(self, event): + menu = self._input_widget.createStandardContextMenu() + menu.setAttribute(QtCore.Qt.WA_DeleteOnClose) + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + menu.popup(event.globalPos()) + def _on_value_change(self): if self.multiline: new_value = self._input_widget.toPlainText() @@ -568,6 +592,20 @@ class BoolAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) self.main_layout.addStretch(1) + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._on_context_menu) + + def _on_context_menu(self, pos): + self._menu = QtWidgets.QMenu(self) + + action = QtWidgets.QAction(self._menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + self._menu.addAction(action) + + global_pos = self.mapToGlobal(pos) + self._menu.exec_(global_pos) + def _on_value_change(self): new_value = self._input_widget.isChecked() self.value_changed.emit(new_value, self.attr_def.id) @@ -631,6 +669,20 @@ class EnumAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) + input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + input_widget.customContextMenuRequested.connect(self._on_context_menu) + + def _on_context_menu(self, pos): + menu = QtWidgets.QMenu(self) + + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + + global_pos = self.mapToGlobal(pos) + menu.exec_(global_pos) + def _on_value_change(self): new_value = self.current_value() if self._multivalue: From cc45af7a96023b4ee9d39e81968bf0cce2290508 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Nov 2024 17:58:55 +0100 Subject: [PATCH 207/546] implemented request revert on files widget --- .../tools/attribute_defs/files_widget.py | 72 ++++++++++++------- .../ayon_core/tools/attribute_defs/widgets.py | 15 ++++ 2 files changed, 60 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 95091bed5a..46399c5fce 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -17,6 +17,8 @@ from ayon_core.tools.utils import ( PixmapLabel ) +from ._constants import REVERT_TO_DEFAULT_LABEL + ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 ITEM_LABEL_ROLE = QtCore.Qt.UserRole + 2 ITEM_ICON_ROLE = QtCore.Qt.UserRole + 3 @@ -598,7 +600,7 @@ class FilesView(QtWidgets.QListView): """View showing instances and their groups.""" remove_requested = QtCore.Signal() - context_menu_requested = QtCore.Signal(QtCore.QPoint) + context_menu_requested = QtCore.Signal(QtCore.QPoint, bool) def __init__(self, *args, **kwargs): super(FilesView, self).__init__(*args, **kwargs) @@ -690,9 +692,8 @@ class FilesView(QtWidgets.QListView): def _on_context_menu_request(self, pos): index = self.indexAt(pos) - if index.isValid(): - point = self.viewport().mapToGlobal(pos) - self.context_menu_requested.emit(point) + point = self.viewport().mapToGlobal(pos) + self.context_menu_requested.emit(point, index.isValid()) def _on_selection_change(self): self._remove_btn.setEnabled(self.has_selected_item_ids()) @@ -721,27 +722,34 @@ class FilesView(QtWidgets.QListView): class FilesWidget(QtWidgets.QFrame): value_changed = QtCore.Signal() + revert_requested = QtCore.Signal() def __init__(self, single_item, allow_sequences, extensions_label, parent): - super(FilesWidget, self).__init__(parent) + super().__init__(parent) self.setAcceptDrops(True) + wrapper_widget = QtWidgets.QWidget(self) + empty_widget = DropEmpty( - single_item, allow_sequences, extensions_label, self + single_item, allow_sequences, extensions_label, wrapper_widget ) files_model = FilesModel(single_item, allow_sequences) files_proxy_model = FilesProxyModel() files_proxy_model.setSourceModel(files_model) - files_view = FilesView(self) + files_view = FilesView(wrapper_widget) files_view.setModel(files_proxy_model) - layout = QtWidgets.QStackedLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) - layout.addWidget(empty_widget) - layout.addWidget(files_view) - layout.setCurrentWidget(empty_widget) + wrapper_layout = QtWidgets.QStackedLayout(wrapper_widget) + wrapper_layout.setContentsMargins(0, 0, 0, 0) + wrapper_layout.setStackingMode(QtWidgets.QStackedLayout.StackAll) + wrapper_layout.addWidget(empty_widget) + wrapper_layout.addWidget(files_view) + wrapper_layout.setCurrentWidget(empty_widget) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(wrapper_widget, 1) files_proxy_model.rowsInserted.connect(self._on_rows_inserted) files_proxy_model.rowsRemoved.connect(self._on_rows_removed) @@ -761,7 +769,11 @@ class FilesWidget(QtWidgets.QFrame): self._widgets_by_id = {} - self._layout = layout + self._wrapper_widget = wrapper_widget + self._wrapper_layout = wrapper_layout + + self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._on_context_menu) def _set_multivalue(self, multivalue): if self._multivalue is multivalue: @@ -770,7 +782,7 @@ class FilesWidget(QtWidgets.QFrame): self._files_view.set_multivalue(multivalue) self._files_model.set_multivalue(multivalue) self._files_proxy_model.set_multivalue(multivalue) - self.setEnabled(not multivalue) + self._wrapper_widget.setEnabled(not multivalue) def set_value(self, value, multivalue): self._in_set_value = True @@ -888,22 +900,28 @@ class FilesWidget(QtWidgets.QFrame): if items_to_delete: self._remove_item_by_ids(items_to_delete) - def _on_context_menu_requested(self, pos): - if self._multivalue: - return + def _on_context_menu(self, pos): + self._on_context_menu_requested(pos, False) + def _on_context_menu_requested(self, pos, valid_index): menu = QtWidgets.QMenu(self._files_view) + if valid_index and not self._multivalue: + if self._files_view.has_selected_sequence(): + split_action = QtWidgets.QAction("Split sequence", menu) + split_action.triggered.connect(self._on_split_request) + menu.addAction(split_action) - if self._files_view.has_selected_sequence(): - split_action = QtWidgets.QAction("Split sequence", menu) - split_action.triggered.connect(self._on_split_request) - menu.addAction(split_action) + remove_action = QtWidgets.QAction("Remove", menu) + remove_action.triggered.connect(self._on_remove_requested) + menu.addAction(remove_action) - remove_action = QtWidgets.QAction("Remove", menu) - remove_action.triggered.connect(self._on_remove_requested) - menu.addAction(remove_action) + if not valid_index: + revert_action = QtWidgets.QAction(REVERT_TO_DEFAULT_LABEL, menu) + revert_action.triggered.connect(self.revert_requested) + menu.addAction(revert_action) - menu.popup(pos) + if menu.actions(): + menu.popup(pos) def dragEnterEvent(self, event): if self._multivalue: @@ -1011,5 +1029,5 @@ class FilesWidget(QtWidgets.QFrame): current_widget = self._files_view else: current_widget = self._empty_widget - self._layout.setCurrentWidget(current_widget) + self._wrapper_layout.setCurrentWidget(current_widget) self._files_view.update_remove_btn_visibility() diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 03482c1006..22f4bfe535 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -811,10 +811,25 @@ class FileAttrWidget(_BaseAttrDefWidget): self.main_layout.addWidget(input_widget, 0) + input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + input_widget.customContextMenuRequested.connect(self._on_context_menu) + input_widget.revert_requested.connect(self.revert_to_default_value) + def _on_value_change(self): new_value = self.current_value() self.value_changed.emit(new_value, self.attr_def.id) + def _on_context_menu(self, pos): + menu = QtWidgets.QMenu(self) + + action = QtWidgets.QAction(menu) + action.setText(REVERT_TO_DEFAULT_LABEL) + action.triggered.connect(self.revert_to_default_value) + menu.addAction(action) + + global_pos = self.mapToGlobal(pos) + menu.exec_(global_pos) + def current_value(self): return self._input_widget.current_value() From 53a839b34fcf04669e094e728448e95ba792d4f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:15:09 +0100 Subject: [PATCH 208/546] fix condition triggering refresh of values in UI --- .../ayon_core/tools/publisher/widgets/product_attributes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/publisher/widgets/product_attributes.py b/client/ayon_core/tools/publisher/widgets/product_attributes.py index 3ff295c986..2b9f316d41 100644 --- a/client/ayon_core/tools/publisher/widgets/product_attributes.py +++ b/client/ayon_core/tools/publisher/widgets/product_attributes.py @@ -232,7 +232,7 @@ class CreatorAttrsWidget(QtWidgets.QWidget): for instance_id, changes in event["instance_changes"].items(): if ( instance_id in self._current_instance_ids - and "creator_attributes" not in changes + and "creator_attributes" in changes ): self._refresh_content() break @@ -498,7 +498,7 @@ class PublishPluginAttrsWidget(QtWidgets.QWidget): for instance_id, changes in event["instance_changes"].items(): if ( instance_id in self._current_instance_ids - and "publish_attributes" not in changes + and "publish_attributes" in changes ): self._refresh_content() break From 569ce30b9672c77e6d553a03098160c0c13e166c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:38:44 +0100 Subject: [PATCH 209/546] pass all required arguments to FileDefItem --- client/ayon_core/lib/attribute_definitions.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 34956fd33f..789c878d40 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -966,7 +966,9 @@ class FileDef(AbstractAttrDef): FileDefItem.from_dict(default) elif isinstance(default, str): - default = FileDefItem.from_paths([default.strip()])[0] + default = FileDefItem.from_paths( + [default.strip()], allow_sequences + )[0] else: raise TypeError(( @@ -1044,7 +1046,9 @@ class FileDef(AbstractAttrDef): pass if string_paths: - file_items = FileDefItem.from_paths(string_paths) + file_items = FileDefItem.from_paths( + string_paths, self.allow_sequences + ) dict_items.extend([ file_item.to_dict() for file_item in file_items From 521d8ed9ec87df5487480ebbfdfac5b31f7dfab4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:28:07 +0100 Subject: [PATCH 210/546] move register functions below classes --- client/ayon_core/lib/attribute_definitions.py | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 34956fd33f..e4e998189d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -15,67 +15,6 @@ import clique _attr_defs_by_type = {} -def register_attr_def_class(cls): - """Register attribute definition. - - Currently registered definitions are used to deserialize data to objects. - - Attrs: - cls (AbstractAttrDef): Non-abstract class to be registered with unique - 'type' attribute. - - Raises: - KeyError: When type was already registered. - """ - - if cls.type in _attr_defs_by_type: - raise KeyError("Type \"{}\" was already registered".format(cls.type)) - _attr_defs_by_type[cls.type] = cls - - -def get_attributes_keys(attribute_definitions): - """Collect keys from list of attribute definitions. - - Args: - attribute_definitions (List[AbstractAttrDef]): Objects of attribute - definitions. - - Returns: - Set[str]: Keys that will be created using passed attribute definitions. - """ - - keys = set() - if not attribute_definitions: - return keys - - for attribute_def in attribute_definitions: - if not isinstance(attribute_def, UIDef): - keys.add(attribute_def.key) - return keys - - -def get_default_values(attribute_definitions): - """Receive default values for attribute definitions. - - Args: - attribute_definitions (List[AbstractAttrDef]): Attribute definitions - for which default values should be collected. - - Returns: - Dict[str, Any]: Default values for passed attribute definitions. - """ - - output = {} - if not attribute_definitions: - return output - - for attr_def in attribute_definitions: - # Skip UI definitions - if not isinstance(attr_def, UIDef): - output[attr_def.key] = attr_def.default - return output - - class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. @@ -1062,6 +1001,67 @@ class FileDef(AbstractAttrDef): return [] +def register_attr_def_class(cls): + """Register attribute definition. + + Currently registered definitions are used to deserialize data to objects. + + Attrs: + cls (AbstractAttrDef): Non-abstract class to be registered with unique + 'type' attribute. + + Raises: + KeyError: When type was already registered. + """ + + if cls.type in _attr_defs_by_type: + raise KeyError("Type \"{}\" was already registered".format(cls.type)) + _attr_defs_by_type[cls.type] = cls + + +def get_attributes_keys(attribute_definitions): + """Collect keys from list of attribute definitions. + + Args: + attribute_definitions (List[AbstractAttrDef]): Objects of attribute + definitions. + + Returns: + Set[str]: Keys that will be created using passed attribute definitions. + """ + + keys = set() + if not attribute_definitions: + return keys + + for attribute_def in attribute_definitions: + if not isinstance(attribute_def, UIDef): + keys.add(attribute_def.key) + return keys + + +def get_default_values(attribute_definitions): + """Receive default values for attribute definitions. + + Args: + attribute_definitions (List[AbstractAttrDef]): Attribute definitions + for which default values should be collected. + + Returns: + Dict[str, Any]: Default values for passed attribute definitions. + """ + + output = {} + if not attribute_definitions: + return output + + for attr_def in attribute_definitions: + # Skip UI definitions + if not isinstance(attr_def, UIDef): + output[attr_def.key] = attr_def.default + return output + + def serialize_attr_def(attr_def): """Serialize attribute definition to data. From 9d629eca2fd87903afd28d2998c4522f8be67fd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:30:55 +0100 Subject: [PATCH 211/546] added helper type definitions --- client/ayon_core/lib/attribute_definitions.py | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e4e998189d..76abe5fe4d 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -6,14 +6,33 @@ import json import copy import warnings from abc import ABCMeta, abstractmethod -from typing import Any, Optional +import typing +from typing import Any, Optional, List, TypedDict import clique +if typing.TYPE_CHECKING: + from typing import Union # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} +# Type hint helpers +IntFloatType = "Union[int, float]" + + +class EnumItemDict(TypedDict): + label: str + value: Any + + +class FileDefItemDict(TypedDict): + directory: str + filenames: List[str] + frames: Optional[List[int]] + template: Optional[str] + is_sequence: Optional[bool] + class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. From 443ebf8523adbbee9112c5f4aa26980f6c3122ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:43:46 +0100 Subject: [PATCH 212/546] added most of typehints --- client/ayon_core/lib/attribute_definitions.py | 177 ++++++++++++------ 1 file changed, 117 insertions(+), 60 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 76abe5fe4d..82c7ab9cb1 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -7,12 +7,14 @@ import copy import warnings from abc import ABCMeta, abstractmethod import typing -from typing import Any, Optional, List, TypedDict +from typing import ( + Any, Optional, List, Set, Dict, Iterable, TypedDict, TypeVar, +) import clique if typing.TYPE_CHECKING: - from typing import Union + from typing import Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -51,8 +53,12 @@ class AbstractAttrDefMeta(ABCMeta): def _convert_reversed_attr( - main_value, depr_value, main_label, depr_label, default -): + main_value: Any, + depr_value: Any, + main_label: str, + depr_label: str, + default: Any, +) -> Any: if main_value is not None and depr_value is not None: if main_value == depr_value: print( @@ -141,7 +147,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): def id(self) -> str: return self._id - def clone(self): + def clone(self) -> "Self": data = self.serialize() data.pop("type") return self.deserialize(data) @@ -214,7 +220,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): pass @abstractmethod - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: """Convert value to a valid one. Convert passed value to a valid type. Use default if value can't be @@ -223,7 +229,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): pass - def serialize(self): + def serialize(self) -> Dict[str, Any]: """Serialize object to data so it's possible to recreate it. Returns: @@ -246,7 +252,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return data @classmethod - def deserialize(cls, data): + def deserialize(cls, data: Dict[str, Any]) -> "Self": """Recreate object from data. Data can be received using 'serialize' method. @@ -257,7 +263,7 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return cls(**data) - def _def_type_compare(self, other: "AbstractAttrDef") -> bool: + def _def_type_compare(self, other: "Self") -> bool: return True @@ -268,13 +274,19 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): class UIDef(AbstractAttrDef): is_value_def = False - def __init__(self, key=None, default=None, *args, **kwargs): + def __init__( + self, + key: Optional[str] = None, + default: Optional[Any] = None, + *args, + **kwargs + ): super().__init__(key, default, *args, **kwargs) def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -305,14 +317,14 @@ class UnknownDef(AbstractAttrDef): type = "unknown" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[Any] = None, **kwargs): kwargs["default"] = default super().__init__(key, **kwargs) def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -327,7 +339,7 @@ class HiddenDef(AbstractAttrDef): type = "hidden" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[Any] = None, **kwargs): kwargs["default"] = default kwargs["visible"] = False super().__init__(key, **kwargs) @@ -335,7 +347,7 @@ class HiddenDef(AbstractAttrDef): def is_value_valid(self, value: Any) -> bool: return True - def convert_value(self, value): + def convert_value(self, value: Any) -> Any: return value @@ -360,7 +372,12 @@ class NumberDef(AbstractAttrDef): ] def __init__( - self, key, minimum=None, maximum=None, decimals=None, default=None, + self, + key: str, + minimum: Optional[IntFloatType] = None, + maximum: Optional[IntFloatType] = None, + decimals: Optional[int] = None, + default: Optional[IntFloatType] = None, **kwargs ): minimum = 0 if minimum is None else minimum @@ -386,9 +403,9 @@ class NumberDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.minimum = minimum - self.maximum = maximum - self.decimals = 0 if decimals is None else decimals + self.minimum: IntFloatType = minimum + self.maximum: IntFloatType = maximum + self.decimals: int = 0 if decimals is None else decimals def is_value_valid(self, value: Any) -> bool: if self.decimals == 0: @@ -400,7 +417,7 @@ class NumberDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value(self, value: Any) -> IntFloatType: if isinstance(value, str): try: value = float(value) @@ -444,7 +461,12 @@ class TextDef(AbstractAttrDef): ] def __init__( - self, key, multiline=None, regex=None, placeholder=None, default=None, + self, + key: str, + multiline: Optional[bool] = None, + regex: Optional[str] = None, + placeholder: Optional[str] = None, + default: Optional[str] = None, **kwargs ): if default is None: @@ -463,9 +485,9 @@ class TextDef(AbstractAttrDef): if isinstance(regex, str): regex = re.compile(regex) - self.multiline = multiline - self.placeholder = placeholder - self.regex = regex + self.multiline: bool = multiline + self.placeholder: Optional[str] = placeholder + self.regex: Optional["Pattern"] = regex def is_value_valid(self, value: Any) -> bool: if not isinstance(value, str): @@ -474,12 +496,12 @@ class TextDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value(self, value: Any) -> str: if isinstance(value, str): return value return self.default - def serialize(self): + def serialize(self) -> Dict[str, Any]: data = super().serialize() regex = None if self.regex is not None: @@ -503,8 +525,9 @@ class EnumDef(AbstractAttrDef): is enabled. Args: - items (Union[list[str], list[dict[str, Any]]): Items definition that - can be converted using 'prepare_enum_items'. + key (str): Key under which value is stored. + items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): Items + definition that can be converted using 'prepare_enum_items'. default (Optional[Any]): Default value. Must be one key(value) from passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. @@ -514,7 +537,12 @@ class EnumDef(AbstractAttrDef): type = "enum" def __init__( - self, key, items, default=None, multiselection=False, **kwargs + self, + key: str, + items: "Union[Dict[Any, str], List[Any], List[EnumItemDict]]", + default: "Union[str, List[Any]]" = None, + multiselection: Optional[bool] = False, + **kwargs ): if not items: raise ValueError(( @@ -525,6 +553,9 @@ class EnumDef(AbstractAttrDef): items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] item_values_set = set(item_values) + if multiselection is None: + multiselection = False + if multiselection: if default is None: default = [] @@ -535,9 +566,9 @@ class EnumDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.items = items - self._item_values = item_values_set - self.multiselection = multiselection + self.items: List[EnumItemDict] = items + self._item_values: Set[Any] = item_values_set + self.multiselection: bool = multiselection def convert_value(self, value): if not self.multiselection: @@ -567,7 +598,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items): + def prepare_enum_items(items) -> List[EnumItemDict]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -583,13 +614,13 @@ class EnumDef(AbstractAttrDef): ``` Args: - items (Union[Dict[str, Any], List[Any], List[Dict[str, Any]]): The + items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): The items to convert. Returns: - List[Dict[str, Any]]: Unified structure of items. - """ + List[EnumItemDict]: Unified structure of items. + """ output = [] if isinstance(items, dict): for value, label in items.items(): @@ -644,7 +675,7 @@ class BoolDef(AbstractAttrDef): type = "bool" - def __init__(self, key, default=None, **kwargs): + def __init__(self, key: str, default: Optional[bool] = None, **kwargs): if default is None: default = False super().__init__(key, default=default, **kwargs) @@ -652,7 +683,7 @@ class BoolDef(AbstractAttrDef): def is_value_valid(self, value: Any) -> bool: return isinstance(value, bool) - def convert_value(self, value): + def convert_value(self, value: Any) -> bool: if isinstance(value, bool): return value return self.default @@ -660,7 +691,11 @@ class BoolDef(AbstractAttrDef): class FileDefItem: def __init__( - self, directory, filenames, frames=None, template=None + self, + directory: str, + filenames: List[str], + frames: Optional[List[int]] = None, + template: Optional[str] = None, ): self.directory = directory @@ -689,7 +724,7 @@ class FileDefItem: ) @property - def label(self): + def label(self) -> Optional[str]: if self.is_empty: return None @@ -732,7 +767,7 @@ class FileDefItem: filename_template, ",".join(ranges) ) - def split_sequence(self): + def split_sequence(self) -> List["Self"]: if not self.is_sequence: raise ValueError("Cannot split single file item") @@ -743,7 +778,7 @@ class FileDefItem: return self.from_paths(paths, False) @property - def ext(self): + def ext(self) -> Optional[str]: if self.is_empty: return None _, ext = os.path.splitext(self.filenames[0]) @@ -752,14 +787,14 @@ class FileDefItem: return None @property - def lower_ext(self): + def lower_ext(self) -> Optional[str]: ext = self.ext if ext is not None: return ext.lower() return ext @property - def is_dir(self): + def is_dir(self) -> bool: if self.is_empty: return False @@ -768,10 +803,15 @@ class FileDefItem: return False return True - def set_directory(self, directory): + def set_directory(self, directory: str): self.directory = directory - def set_filenames(self, filenames, frames=None, template=None): + def set_filenames( + self, + filenames: List[str], + frames: Optional[List[int]] = None, + template: Optional[str] = None, + ): if frames is None: frames = [] is_sequence = False @@ -788,11 +828,15 @@ class FileDefItem: self.is_sequence = is_sequence @classmethod - def create_empty_item(cls): + def create_empty_item(cls) -> "Self": return cls("", "") @classmethod - def from_value(cls, value, allow_sequences): + def from_value( + cls, + value: "Union[List[FileDefItemDict], FileDefItemDict]", + allow_sequences: bool, + ) -> List["Self"]: """Convert passed value to FileDefItem objects. Returns: @@ -830,7 +874,7 @@ class FileDefItem: return output @classmethod - def from_dict(cls, data): + def from_dict(cls, data: FileDefItemDict) -> "Self": return cls( data["directory"], data["filenames"], @@ -839,7 +883,11 @@ class FileDefItem: ) @classmethod - def from_paths(cls, paths, allow_sequences): + def from_paths( + cls, + paths: List[str], + allow_sequences: bool, + ) -> List["Self"]: filenames_by_dir = collections.defaultdict(list) for path in paths: normalized = os.path.normpath(path) @@ -868,7 +916,7 @@ class FileDefItem: return output - def to_dict(self): + def to_dict(self) -> FileDefItemDict: output = { "is_sequence": self.is_sequence, "directory": self.directory, @@ -906,8 +954,15 @@ class FileDef(AbstractAttrDef): ] def __init__( - self, key, single_item=True, folders=None, extensions=None, - allow_sequences=True, extensions_label=None, default=None, **kwargs + self, + key: str, + single_item: Optional[bool] = True, + folders: Optional[bool] = None, + extensions: Optional[Iterable[str]] = None, + allow_sequences: Optional[bool] = True, + extensions_label: Optional[str] = None, + default: Optional["Union[FileDefItemDict, List[str]]"] = None, + **kwargs ): if folders is None and extensions is None: folders = True @@ -943,14 +998,14 @@ class FileDef(AbstractAttrDef): if is_label_horizontal is None: kwargs["is_label_horizontal"] = False - self.single_item = single_item - self.folders = folders - self.extensions = set(extensions) - self.allow_sequences = allow_sequences - self.extensions_label = extensions_label + self.single_item: bool = single_item + self.folders: bool = folders + self.extensions: Set[str] = set(extensions) + self.allow_sequences: bool = allow_sequences + self.extensions_label: Optional[str] = extensions_label super().__init__(key, default=default, **kwargs) - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if not super().__eq__(other): return False @@ -984,7 +1039,9 @@ class FileDef(AbstractAttrDef): return False return True - def convert_value(self, value): + def convert_value( + self, value: Any + ) -> "Union[FileDefItemDict, List[FileDefItemDict]]": if isinstance(value, (str, dict)): value = [value] From 586d29f219f76572ffea9c431fe9f197cd0a2907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:44:32 +0100 Subject: [PATCH 213/546] define 'EnumItemsInputType' for EnumDef input items --- client/ayon_core/lib/attribute_definitions.py | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 82c7ab9cb1..bf47b7617b 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -8,13 +8,22 @@ import warnings from abc import ABCMeta, abstractmethod import typing from typing import ( - Any, Optional, List, Set, Dict, Iterable, TypedDict, TypeVar, + Any, + Optional, + Tuple, + List, + Set, + Dict, + Iterable, + TypedDict, + TypeVar, ) import clique if typing.TYPE_CHECKING: from typing import Self, Union, Pattern + # Global variable which store attribute definitions by type # - default types are registered on import _attr_defs_by_type = {} @@ -28,6 +37,9 @@ class EnumItemDict(TypedDict): value: Any +EnumItemsInputType = "Union[Dict[Any, str], List[Tuple[Any, str]], List[Any], List[EnumItemDict]]" # noqa: E501 + + class FileDefItemDict(TypedDict): directory: str filenames: List[str] @@ -526,8 +538,8 @@ class EnumDef(AbstractAttrDef): Args: key (str): Key under which value is stored. - items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): Items - definition that can be converted using 'prepare_enum_items'. + items (EnumItemsInputType): Items definition that can be converted + using 'prepare_enum_items'. default (Optional[Any]): Default value. Must be one key(value) from passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. @@ -539,7 +551,7 @@ class EnumDef(AbstractAttrDef): def __init__( self, key: str, - items: "Union[Dict[Any, str], List[Any], List[EnumItemDict]]", + items: EnumItemsInputType, default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, **kwargs @@ -598,7 +610,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items) -> List[EnumItemDict]: + def prepare_enum_items(items: EnumItemsInputType) -> List[EnumItemDict]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -614,8 +626,7 @@ class EnumDef(AbstractAttrDef): ``` Args: - items (Union[Dict[Any, str], List[Any], List[EnumItemDict]]): The - items to convert. + items (EnumItemsInputType): The items to convert. Returns: List[EnumItemDict]: Unified structure of items. From b2a9277267a36fbbad093a5fc94b5f33e286de0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:55:48 +0100 Subject: [PATCH 214/546] define 'AttrDefType' --- client/ayon_core/lib/attribute_definitions.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index bf47b7617b..836d6c7463 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -279,6 +279,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): return True +AttrDefType = TypeVar("AttrDefType", bound=AbstractAttrDef) + # ----------------------------------------- # UI attribute definitions won't hold value # ----------------------------------------- @@ -1088,13 +1090,13 @@ class FileDef(AbstractAttrDef): return [] -def register_attr_def_class(cls): +def register_attr_def_class(cls: AttrDefType): """Register attribute definition. Currently registered definitions are used to deserialize data to objects. Attrs: - cls (AbstractAttrDef): Non-abstract class to be registered with unique + cls (AttrDefType): Non-abstract class to be registered with unique 'type' attribute. Raises: @@ -1106,11 +1108,13 @@ def register_attr_def_class(cls): _attr_defs_by_type[cls.type] = cls -def get_attributes_keys(attribute_definitions): +def get_attributes_keys( + attribute_definitions: List[AttrDefType] +) -> Set[str]: """Collect keys from list of attribute definitions. Args: - attribute_definitions (List[AbstractAttrDef]): Objects of attribute + attribute_definitions (List[AttrDefType]): Objects of attribute definitions. Returns: @@ -1127,11 +1131,13 @@ def get_attributes_keys(attribute_definitions): return keys -def get_default_values(attribute_definitions): +def get_default_values( + attribute_definitions: List[AttrDefType] +) -> Dict[str, Any]: """Receive default values for attribute definitions. Args: - attribute_definitions (List[AbstractAttrDef]): Attribute definitions + attribute_definitions (List[AttrDefType]): Attribute definitions for which default values should be collected. Returns: @@ -1149,11 +1155,11 @@ def get_default_values(attribute_definitions): return output -def serialize_attr_def(attr_def): +def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]: """Serialize attribute definition to data. Args: - attr_def (AbstractAttrDef): Attribute definition to serialize. + attr_def (AttrDefType): Attribute definition to serialize. Returns: Dict[str, Any]: Serialized data. @@ -1162,11 +1168,13 @@ def serialize_attr_def(attr_def): return attr_def.serialize() -def serialize_attr_defs(attr_defs): +def serialize_attr_defs( + attr_defs: List[AttrDefType] +) -> List[Dict[str, Any]]: """Serialize attribute definitions to data. Args: - attr_defs (List[AbstractAttrDef]): Attribute definitions to serialize. + attr_defs (List[AttrDefType]): Attribute definitions to serialize. Returns: List[Dict[str, Any]]: Serialized data. @@ -1178,7 +1186,7 @@ def serialize_attr_defs(attr_defs): ] -def deserialize_attr_def(attr_def_data): +def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType: """Deserialize attribute definition from data. Args: @@ -1191,7 +1199,9 @@ def deserialize_attr_def(attr_def_data): return cls.deserialize(attr_def_data) -def deserialize_attr_defs(attr_defs_data): +def deserialize_attr_defs( + attr_defs_data: List[Dict[str, Any]] +) -> List[AttrDefType]: """Deserialize attribute definitions. Args: From 341dc04cabd6982b636689cd63e83f3a9f0b3a5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:55:59 +0100 Subject: [PATCH 215/546] change formatting of docstrings --- client/ayon_core/lib/attribute_definitions.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 836d6c7463..9e1a92b18e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -52,8 +52,8 @@ class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. Each object of `AbstractAttrDef` must have defined 'key' attribute. - """ + """ def __call__(cls, *args, **kwargs): obj = super(AbstractAttrDefMeta, cls).__call__(*args, **kwargs) init_class = getattr(obj, "__init__class__", None) @@ -116,8 +116,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): enabled (Optional[bool]): Item is enabled (for UI purposes). hidden (Optional[bool]): DEPRECATED: Use 'visible' instead. disabled (Optional[bool]): DEPRECATED: Use 'enabled' instead. - """ + """ type_attributes = [] is_value_def = True @@ -227,8 +227,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Returns: str: Type of attribute definition. - """ + """ pass @abstractmethod @@ -237,8 +237,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Convert passed value to a valid type. Use default if value can't be converted. - """ + """ pass def serialize(self) -> Dict[str, Any]: @@ -247,8 +247,8 @@ class AbstractAttrDef(metaclass=AbstractAttrDefMeta): Returns: Dict[str, Any]: Serialized object that can be passed to 'deserialize' method. - """ + """ data = { "type": self.type, "key": self.key, @@ -327,8 +327,8 @@ class UnknownDef(AbstractAttrDef): This attribute can be used to keep existing data unchanged but does not have known definition of type. - """ + """ type = "unknown" def __init__(self, key: str, default: Optional[Any] = None, **kwargs): @@ -349,8 +349,8 @@ class HiddenDef(AbstractAttrDef): to other attributes (e.g. in multi-page UIs). Keep in mind the value should be possible to parse by json parser. - """ + """ type = "hidden" def __init__(self, key: str, default: Optional[Any] = None, **kwargs): @@ -376,8 +376,8 @@ class NumberDef(AbstractAttrDef): maximum(int, float): Maximum possible value. decimals(int): Maximum decimal points of value. default(int, float): Default value for conversion. - """ + """ type = "number" type_attributes = [ "minimum", @@ -466,8 +466,8 @@ class TextDef(AbstractAttrDef): regex(str, re.Pattern): Regex validation. placeholder(str): UI placeholder for attribute. default(str, None): Default value. Empty string used when not defined. - """ + """ type = "text" type_attributes = [ "multiline", @@ -546,8 +546,8 @@ class EnumDef(AbstractAttrDef): passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. Output is list of selected items. - """ + """ type = "enum" def __init__( @@ -684,8 +684,8 @@ class BoolDef(AbstractAttrDef): Args: default(bool): Default value. Set to `False` if not defined. - """ + """ type = "bool" def __init__(self, key: str, default: Optional[bool] = None, **kwargs): @@ -854,8 +854,8 @@ class FileDefItem: Returns: list: Created FileDefItem objects. - """ + """ # Convert single item to iterable if not isinstance(value, (list, tuple, set)): value = [value] @@ -1101,8 +1101,8 @@ def register_attr_def_class(cls: AttrDefType): Raises: KeyError: When type was already registered. - """ + """ if cls.type in _attr_defs_by_type: raise KeyError("Type \"{}\" was already registered".format(cls.type)) _attr_defs_by_type[cls.type] = cls @@ -1119,8 +1119,8 @@ def get_attributes_keys( Returns: Set[str]: Keys that will be created using passed attribute definitions. - """ + """ keys = set() if not attribute_definitions: return keys @@ -1142,8 +1142,8 @@ def get_default_values( Returns: Dict[str, Any]: Default values for passed attribute definitions. - """ + """ output = {} if not attribute_definitions: return output @@ -1163,8 +1163,8 @@ def serialize_attr_def(attr_def: AttrDefType) -> Dict[str, Any]: Returns: Dict[str, Any]: Serialized data. - """ + """ return attr_def.serialize() @@ -1178,8 +1178,8 @@ def serialize_attr_defs( Returns: List[Dict[str, Any]]: Serialized data. - """ + """ return [ serialize_attr_def(attr_def) for attr_def in attr_defs @@ -1192,8 +1192,8 @@ def deserialize_attr_def(attr_def_data: Dict[str, Any]) -> AttrDefType: Args: attr_def_data (Dict[str, Any]): Attribute definition data to deserialize. - """ + """ attr_type = attr_def_data.pop("type") cls = _attr_defs_by_type[attr_type] return cls.deserialize(attr_def_data) @@ -1206,8 +1206,8 @@ def deserialize_attr_defs( Args: List[Dict[str, Any]]: List of attribute definitions. - """ + """ return [ deserialize_attr_def(attr_def_data) for attr_def_data in attr_defs_data From 683bc0e39a303189149ea86f3db9747e9cb0a498 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 13:51:54 +0100 Subject: [PATCH 216/546] fix import --- client/ayon_core/lib/attribute_definitions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 9e1a92b18e..68c84276cb 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -10,7 +10,6 @@ import typing from typing import ( Any, Optional, - Tuple, List, Set, Dict, @@ -22,7 +21,7 @@ from typing import ( import clique if typing.TYPE_CHECKING: - from typing import Self, Union, Pattern + from typing import Tuple, Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import From 07bbe08c76e58f835c1892af63395979dcfbf26b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:08:08 +0100 Subject: [PATCH 217/546] remove 'Tuple' import Looks like the import is not needed even if the typehint is used for 'EnumItemsInputType'? --- client/ayon_core/lib/attribute_definitions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index 68c84276cb..e841a4b230 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -21,7 +21,7 @@ from typing import ( import clique if typing.TYPE_CHECKING: - from typing import Tuple, Self, Union, Pattern + from typing import Self, Union, Pattern # Global variable which store attribute definitions by type # - default types are registered on import From 68db3d9c117df46aaf883a344aef61d26752aa22 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:48:19 +0100 Subject: [PATCH 218/546] Add logic to extract colorspace from metadata if available. - Extract colorspace from media metadata for review clips. - Update instance data with the extracted colorspace information. --- .../plugins/publish/collect_otio_review.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 69cf9199e7..04422391c5 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -95,9 +95,46 @@ class CollectOtioReview(pyblish.api.InstancePlugin): instance.data["label"] = label + " (review)" instance.data["families"] += ["review", "ftrack"] instance.data["otioReviewClips"] = otio_review_clips + self.log.info( "Creating review track: {}".format(otio_review_clips)) + # get colorspace from metadata if available + if len(otio_review_clips) >= 1 and any( + # lets make sure any clip with media reference is found + ( + clip + for clip in otio_review_clips + if isinstance(clip, otio.schema.Clip) + and clip.media_reference + ) + ): + # get metadata from first clip + # get colorspace from metadata if available + # check if resolution is the same as source + r_otio_cl = next( + ( + clip + for clip in otio_review_clips + if isinstance(clip, otio.schema.Clip) + and clip.media_reference + ), + None, + ) + + # get metadata from first clip with media reference + media_ref = r_otio_cl.media_reference + media_metadata = media_ref.metadata + + # TODO: we might need some alternative method since + # native OTIO exports do not support ayon metadata + if review_colorspace := media_metadata.get( + "ayon.source.colorspace" + ): + instance.data["reviewColorspace"] = review_colorspace + self.log.info( + "Review colorspace: {}".format(review_colorspace)) + self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) self.log.debug( From 83f28bf184bfa514294133f01787a47758ca610d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:48:30 +0100 Subject: [PATCH 219/546] Refactor plugin to include Colormanaged mixin The code changes refactor the plugin to include a Colormanaged mixin for managing colorspace data in representations. The mixin is added to the existing plugin class. --- .../plugins/publish/collect_otio_subset_resources.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 37a5e87a7a..c142036b83 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -10,12 +10,16 @@ import os import clique import pyblish.api +from ayon_core.pipeline import publish from ayon_core.pipeline.publish import ( get_publish_template_name ) -class CollectOtioSubsetResources(pyblish.api.InstancePlugin): +class CollectOtioSubsetResources( + pyblish.api.InstancePlugin, + publish.ColormanagedPyblishPluginMixin +): """Get Resources for a product version""" label = "Collect OTIO Subset Resources" @@ -190,9 +194,13 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): instance.data["originalDirname"] = self.staging_dir if repre: + colorspace = instance.data.get("colorspace") + # add colorspace data to representation + self.set_representation_colorspace( + repre, instance.context, colorspace) + # add representation to instance data instance.data["representations"].append(repre) - self.log.debug(">>>>>>>> {}".format(repre)) self.log.debug(instance.data) From 6a635b9d5e0852a77bd2bfaaa28b3ec6d1e8b4d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:49:03 +0100 Subject: [PATCH 220/546] Update color transcoding process with debug log messages. - Add debug logs for files to convert, transcoded file, and input path. --- client/ayon_core/plugins/publish/extract_color_transcode.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 3e54d324e3..e7e0c982eb 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -5,7 +5,6 @@ import pyblish.api from ayon_core.pipeline import publish from ayon_core.lib import ( - is_oiio_supported, ) @@ -154,12 +153,15 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) + self.log.debug("Files to convert: {}".format(files_to_convert)) for file_name in files_to_convert: + self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, file_name) output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) + self.log.debug("Ynput path: `{}`".format(input_path)) convert_colorspace( input_path, output_path, From 3a71bbca295d8d2b9d7ab452ac1b3b8f3f26037c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:49:13 +0100 Subject: [PATCH 221/546] Add colorspace data extraction to representation loop Extracts colorspace data from instance data and sets it in the representation loop for processing. --- client/ayon_core/plugins/publish/extract_colorspace_data.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_colorspace_data.py b/client/ayon_core/plugins/publish/extract_colorspace_data.py index 7da4890748..d68ad4d80d 100644 --- a/client/ayon_core/plugins/publish/extract_colorspace_data.py +++ b/client/ayon_core/plugins/publish/extract_colorspace_data.py @@ -37,6 +37,9 @@ class ExtractColorspaceData(publish.Extractor, # get colorspace settings context = instance.context + # colorspace name could be kept in instance.data + colorspace = instance.data.get("colorspace") + # loop representations for representation in representations: # skip if colorspaceData is already at representation @@ -44,5 +47,5 @@ class ExtractColorspaceData(publish.Extractor, continue self.set_representation_colorspace( - representation, context + representation, context, colorspace) ) From 0ff9ae65d8843afa0179277c2c3342fab465cec9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:50:38 +0100 Subject: [PATCH 222/546] Refactor ExtractOTIOReview class inheritance and add colorspace handling - Refactored class inheritance for ExtractOTIOReview - Added handling for colorspace data in representation creation --- .../plugins/publish/extract_otio_review.py | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index faba9fd36d..2c6472f8a4 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -26,7 +26,10 @@ from ayon_core.lib import ( from ayon_core.pipeline import publish -class ExtractOTIOReview(publish.Extractor): +class ExtractOTIOReview( + publish.Extractor, + publish.ColormanagedPyblishPluginMixin +): """ Extract OTIO timeline into one concuted image sequence file. @@ -78,7 +81,9 @@ class ExtractOTIOReview(publish.Extractor): self.used_frames = [] self.workfile_start = int(instance.data.get( "workfileFrameStart", 1001)) - handle_start - self.padding = len(str(self.workfile_start)) + # NOTE: padding has to be converted from + # end frame since start could be lower then 1000 + self.padding = len(str(instance.data.get("frameEnd", 1001))) self.used_frames.append(self.workfile_start) self.to_width = instance.data.get( "resolutionWidth") or self.to_width @@ -86,8 +91,10 @@ class ExtractOTIOReview(publish.Extractor): "resolutionHeight") or self.to_height # skip instance if no reviewable data available - if (not isinstance(otio_review_clips[0], otio.schema.Clip)) \ - and (len(otio_review_clips) == 1): + if ( + not isinstance(otio_review_clips[0], otio.schema.Clip) + and len(otio_review_clips) == 1 + ): self.log.warning( "Instance `{}` has nothing to process".format(instance)) return @@ -168,7 +175,7 @@ class ExtractOTIOReview(publish.Extractor): start -= clip_handle_start duration += clip_handle_start elif len(otio_review_clips) > 1 \ - and (index == len(otio_review_clips) - 1): + and (index == len(otio_review_clips) - 1): # more clips | last clip reframing with handle duration += clip_handle_end elif len(otio_review_clips) == 1: @@ -263,6 +270,13 @@ class ExtractOTIOReview(publish.Extractor): # creating and registering representation representation = self._create_representation(start, duration) + + # add colorspace data to representation + if colorspace := instance.data.get("reviewColorspace"): + self.set_representation_colorspace( + representation, instance.context, colorspace + ) + instance.data["representations"].append(representation) self.log.info("Adding representation: {}".format(representation)) From e0e541b24a01110846ff57b359a66ed8b60af81c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 6 Nov 2024 15:57:24 +0100 Subject: [PATCH 223/546] Refactor colorspace extraction logic - Removed unnecessary closing parenthesis in colorspace extraction method. --- client/ayon_core/plugins/publish/extract_colorspace_data.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_colorspace_data.py b/client/ayon_core/plugins/publish/extract_colorspace_data.py index d68ad4d80d..0ffa0f3035 100644 --- a/client/ayon_core/plugins/publish/extract_colorspace_data.py +++ b/client/ayon_core/plugins/publish/extract_colorspace_data.py @@ -48,4 +48,3 @@ class ExtractColorspaceData(publish.Extractor, self.set_representation_colorspace( representation, context, colorspace) - ) From 46c6511c500804c6d690aaab02ac6c02bdf22b5d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 7 Nov 2024 09:25:22 +0100 Subject: [PATCH 224/546] Refactor debug log in color transcoding function Removed unnecessary debug log statement from color transcoding function. --- client/ayon_core/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index e7e0c982eb..56d5d33ea4 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -161,7 +161,7 @@ class ExtractOIIOTranscode(publish.Extractor): output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) - self.log.debug("Ynput path: `{}`".format(input_path)) + convert_colorspace( input_path, output_path, From ed9b8fe430e1d5c21a3946f548b57c3f8b1d056f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:08:04 +0100 Subject: [PATCH 225/546] moved TypedDict to typecheck imports --- client/ayon_core/lib/attribute_definitions.py | 51 ++++++++++--------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e841a4b230..02d468f1bb 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -14,14 +14,35 @@ from typing import ( Set, Dict, Iterable, - TypedDict, TypeVar, ) import clique if typing.TYPE_CHECKING: - from typing import Self, Union, Pattern + from typing import Self, Tuple, Union, TypedDict, Pattern + + + class EnumItemDict(TypedDict): + label: str + value: Any + + + EnumItemsInputType = Union[ + Dict[Any, str], + List[Tuple[Any, str]], + List[Any], + List[EnumItemDict] + ] + + + class FileDefItemDict(TypedDict): + directory: str + filenames: List[str] + frames: Optional[List[int]] + template: Optional[str] + is_sequence: Optional[bool] + # Global variable which store attribute definitions by type # - default types are registered on import @@ -31,22 +52,6 @@ _attr_defs_by_type = {} IntFloatType = "Union[int, float]" -class EnumItemDict(TypedDict): - label: str - value: Any - - -EnumItemsInputType = "Union[Dict[Any, str], List[Tuple[Any, str]], List[Any], List[EnumItemDict]]" # noqa: E501 - - -class FileDefItemDict(TypedDict): - directory: str - filenames: List[str] - frames: Optional[List[int]] - template: Optional[str] - is_sequence: Optional[bool] - - class AbstractAttrDefMeta(ABCMeta): """Metaclass to validate the existence of 'key' attribute. @@ -552,7 +557,7 @@ class EnumDef(AbstractAttrDef): def __init__( self, key: str, - items: EnumItemsInputType, + items: "EnumItemsInputType", default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, **kwargs @@ -579,7 +584,7 @@ class EnumDef(AbstractAttrDef): super().__init__(key, default=default, **kwargs) - self.items: List[EnumItemDict] = items + self.items: List["EnumItemDict"] = items self._item_values: Set[Any] = item_values_set self.multiselection: bool = multiselection @@ -611,7 +616,7 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items: EnumItemsInputType) -> List[EnumItemDict]: + def prepare_enum_items(items: "EnumItemsInputType") -> List["EnumItemDict"]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' @@ -886,7 +891,7 @@ class FileDefItem: return output @classmethod - def from_dict(cls, data: FileDefItemDict) -> "Self": + def from_dict(cls, data: "FileDefItemDict") -> "Self": return cls( data["directory"], data["filenames"], @@ -928,7 +933,7 @@ class FileDefItem: return output - def to_dict(self) -> FileDefItemDict: + def to_dict(self) -> "FileDefItemDict": output = { "is_sequence": self.is_sequence, "directory": self.directory, From ad25aa7b525276da52bf80e2686a0482954a1bec Mon Sep 17 00:00:00 2001 From: Joseph HENRY Date: Thu, 7 Nov 2024 12:47:24 +0100 Subject: [PATCH 226/546] Use open -R for opening explorer on MacOS --- client/ayon_core/plugins/actions/open_file_explorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index 50a3107444..e96392ec00 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -99,7 +99,7 @@ class OpenTaskPath(LauncherAction): if platform_name == "windows": args = ["start", path] elif platform_name == "darwin": - args = ["open", "-na", path] + args = ["open", "-R", path] elif platform_name == "linux": args = ["xdg-open", path] else: From 7d23e1ac3fc29a8e8bc99ef94283f62f2b9f746f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 7 Nov 2024 16:36:47 +0100 Subject: [PATCH 227/546] Fix support for scriptsmenu running commands in Qt6 (e.g. PySide6 in Maya 2025) --- client/ayon_core/vendor/python/scriptsmenu/action.py | 9 +++++---- .../ayon_core/vendor/python/scriptsmenu/launchformaya.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/vendor/python/scriptsmenu/action.py b/client/ayon_core/vendor/python/scriptsmenu/action.py index 49b08788f9..3ba281fed7 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/action.py +++ b/client/ayon_core/vendor/python/scriptsmenu/action.py @@ -1,6 +1,6 @@ import os -from qtpy import QtWidgets +from qtpy import QtWidgets, QT6 class Action(QtWidgets.QAction): @@ -112,20 +112,21 @@ module.{module_name}()""" Run the command of the instance or copy the command to the active shelf based on the current modifiers. - If callbacks have been registered with fouind modifier integer the + If callbacks have been registered with found modifier integer the function will trigger all callbacks. When a callback function returns a non zero integer it will not execute the action's command - """ # get the current application and its linked keyboard modifiers app = QtWidgets.QApplication.instance() modifiers = app.keyboardModifiers() + if not QT6: + modifiers = int(modifiers) # If the menu has a callback registered for the current modifier # we run the callback instead of the action itself. registered = self._root.registered_callbacks - callbacks = registered.get(int(modifiers), []) + callbacks = registered.get(modifiers, []) for callback in callbacks: signal = callback(self) if signal != 0: diff --git a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py index 496278ac6f..a5503bc63e 100644 --- a/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py +++ b/client/ayon_core/vendor/python/scriptsmenu/launchformaya.py @@ -4,7 +4,7 @@ import maya.cmds as cmds import maya.mel as mel import scriptsmenu -from qtpy import QtCore, QtWidgets +from qtpy import QtCore, QtWidgets, QT6 log = logging.getLogger(__name__) @@ -130,7 +130,7 @@ def main(title="Scripts", parent=None, objectName=None): # Register control + shift callback to add to shelf (maya behavior) modifiers = QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier - if int(cmds.about(version=True)) < 2025: + if not QT6: modifiers = int(modifiers) menu.register_callback(modifiers, to_shelf) From 87bb613b751ea508ae54a9813bf6eb5852ca5b6b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 7 Nov 2024 17:38:00 +0100 Subject: [PATCH 228/546] Added optionality to new argument in method signature --- client/ayon_core/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index c70967dfc1..e9f179c668 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -304,7 +304,7 @@ def prepare_representations( do_not_add_review, context, color_managed_plugin, - frames_to_render + frames_to_render=None ): """Create representations for file sequences. From 2337d116d54eaabfd73b81f9f45c1865e124a65a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:46:53 +0100 Subject: [PATCH 229/546] change is_latest based on version item --- client/ayon_core/tools/sceneinventory/model.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index b7f79986ac..9b1e75a0d1 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -194,14 +194,14 @@ class InventoryModel(QtGui.QStandardItemModel): group_items = [] for repre_id, container_items in items_by_repre_id.items(): repre_info = repre_info_by_id[repre_id] - version_label = "N/A" version_color = None - is_latest = False - is_hero = False - status_name = None if not repre_info.is_valid: + version_label = "N/A" group_name = "< Entity N/A >" item_icon = invalid_item_icon + is_latest = False + is_hero = False + status_name = None else: group_name = "{}_{}: ({})".format( @@ -217,6 +217,7 @@ class InventoryModel(QtGui.QStandardItemModel): version_item = version_items[repre_info.version_id] version_label = format_version(version_item.version) is_hero = version_item.version < 0 + is_latest = version_item.is_latest if not version_item.is_latest: version_color = self.OUTDATED_COLOR status_name = version_item.status From 8a7239fc0511c19bfb1f1a0bf0e01d18b9026fa8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:47:04 +0100 Subject: [PATCH 230/546] remove unncessary line --- client/ayon_core/tools/sceneinventory/models/containers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 871455c96b..4f3ddf1ded 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -383,7 +383,6 @@ class ContainersModel: container_items_by_id[item.item_id] = item container_items.append(item) - self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id self._items_cache = container_items From 1de069c324a8d49c7e1424fe7d0fd95539238145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Nov 2024 18:47:15 +0100 Subject: [PATCH 231/546] remove unnessary conversion --- client/ayon_core/tools/sceneinventory/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 9b1e75a0d1..bdcd183c99 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -426,7 +426,7 @@ class FilterProxyModel(QtCore.QSortFilterProxyModel): state = bool(state) if state != self._filter_outdated: - self._filter_outdated = bool(state) + self._filter_outdated = state self.invalidateFilter() def set_hierarchy_view(self, state): From 749984c0bff74c4491a7d6e853afa70906b1e984 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 8 Nov 2024 13:10:44 +0100 Subject: [PATCH 232/546] Fix loader load option box widgets --- client/ayon_core/tools/attribute_defs/widgets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 22f4bfe535..93f63730f5 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -202,7 +202,6 @@ class AttributeDefinitionsWidget(QtWidgets.QWidget): self._current_keys.add(attr_def.key) widget = create_widget_for_attr_def(attr_def, self) - self._widgets.append(widget) self._widgets_by_id[attr_def.id] = widget if not attr_def.visible: From 20206a3cf3444c3a74b8f3aa046985def1cbfa38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:57:00 +0100 Subject: [PATCH 233/546] check executable name before killing the process --- client/ayon_core/tools/tray/lib.py | 103 ++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 39fcc2cdd3..94550775e6 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -3,12 +3,9 @@ import sys import json import hashlib import platform -import subprocess -import csv import time import signal -import locale -from typing import Optional, Dict, Tuple, Any +from typing import Optional, List, Dict, Tuple, Any import requests from ayon_api.utils import get_default_settings_variant @@ -53,15 +50,99 @@ def _get_server_and_variant( return server_url, variant +def _windows_get_pid_args(pid: int) -> Optional[List[str]]: + import ctypes + from ctypes import wintypes + + # Define constants + PROCESS_COMMANDLINE_INFO = 60 + STATUS_NOT_FOUND = 0xC0000225 + PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 + + # Define the UNICODE_STRING structure + class UNICODE_STRING(ctypes.Structure): + _fields_ = [ + ("Length", wintypes.USHORT), + ("MaximumLength", wintypes.USHORT), + ("Buffer", wintypes.LPWSTR) + ] + + shell32 = ctypes.WinDLL("shell32", use_last_error=True) + + CommandLineToArgvW = shell32.CommandLineToArgvW + CommandLineToArgvW.argtypes = [ + wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int) + ] + CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR) + + output = None + # Open the process + handle = ctypes.windll.kernel32.OpenProcess( + PROCESS_QUERY_LIMITED_INFORMATION, False, pid + ) + if not handle: + return output + + try: + buffer_len = wintypes.ULONG() + # Get the right buffer size first + status = ctypes.windll.ntdll.NtQueryInformationProcess( + handle, + PROCESS_COMMANDLINE_INFO, + ctypes.c_void_p(None), + 0, + ctypes.byref(buffer_len) + ) + + if status == STATUS_NOT_FOUND: + return output + + # Create buffer with collected size + buffer = ctypes.create_string_buffer(buffer_len.value) + + # Get the command line + status = ctypes.windll.ntdll.NtQueryInformationProcess( + handle, + PROCESS_COMMANDLINE_INFO, + buffer, + buffer_len, + ctypes.byref(buffer_len) + ) + if status: + return output + # Build the string + tmp = ctypes.cast(buffer, ctypes.POINTER(UNICODE_STRING)).contents + size = tmp.Length // 2 + 1 + cmdline_buffer = ctypes.create_unicode_buffer(size) + ctypes.cdll.msvcrt.wcscpy(cmdline_buffer, tmp.Buffer) + + args_len = ctypes.c_int() + args = CommandLineToArgvW( + cmdline_buffer, ctypes.byref(args_len) + ) + output = [args[idx] for idx in range(args_len.value)] + ctypes.windll.kernel32.LocalFree(args) + + finally: + ctypes.windll.kernel32.CloseHandle(handle) + return output def _windows_pid_is_running(pid: int) -> bool: - args = ["tasklist.exe", "/fo", "csv", "/fi", f"PID eq {pid}"] - output = subprocess.check_output(args) - encoding = locale.getpreferredencoding() - csv_content = csv.DictReader(output.decode(encoding).splitlines()) - # if "PID" not in csv_content.fieldnames: - # return False - for _ in csv_content: + args = _windows_get_pid_args(pid) + if not args: + return False + executable_path = args[0] + + filename = os.path.basename(executable_path).lower() + if "ayon" in filename: return True + + # Try to handle tray running from code + # - this might be potential danger that kills other python process running + # 'start.py' script (low chance, but still) + if "python" in filename and len(args) > 1: + script_filename = os.path.basename(args[1].lower()) + if script_filename == "start.py": + return True return False From 41db386f23f4ec18e870abe1817d0f71eb8fc775 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 8 Nov 2024 16:59:05 +0100 Subject: [PATCH 234/546] add empty lines --- client/ayon_core/tools/tray/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/tray/lib.py b/client/ayon_core/tools/tray/lib.py index 94550775e6..13ee1eea5c 100644 --- a/client/ayon_core/tools/tray/lib.py +++ b/client/ayon_core/tools/tray/lib.py @@ -126,6 +126,8 @@ def _windows_get_pid_args(pid: int) -> Optional[List[str]]: finally: ctypes.windll.kernel32.CloseHandle(handle) return output + + def _windows_pid_is_running(pid: int) -> bool: args = _windows_get_pid_args(pid) if not args: From 262cc0e7bb117516b1c1a3a7ef3b71d0508c8adf Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 8 Nov 2024 18:07:10 +0000 Subject: [PATCH 235/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 2b2af81e18..74f64e7944 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.6+dev" +__version__ = "1.0.7" diff --git a/package.py b/package.py index 59f0e82be0..c3fc02b625 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.6+dev" +version = "1.0.7" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ca626eff00..12a68630e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.6+dev" +version = "1.0.7" description = "" authors = ["Ynput Team "] readme = "README.md" From 7ae9b1815378352ed86f7b0dee251d58995bf11a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 8 Nov 2024 18:07:45 +0000 Subject: [PATCH 236/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 74f64e7944..3a5b63785d 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.7" +__version__ = "1.0.7+dev" diff --git a/package.py b/package.py index c3fc02b625..ef2f3822eb 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.7" +version = "1.0.7+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 12a68630e2..78a3021b30 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.7" +version = "1.0.7+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From aaadaffabe5aeb033f3b1f7e0fe3341c69356564 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Nov 2024 16:57:41 +0800 Subject: [PATCH 237/546] refactoring the load container so that it can load the library project --- client/ayon_core/pipeline/load/utils.py | 4 +- .../ayon_core/tools/sceneinventory/control.py | 7 +- .../ayon_core/tools/sceneinventory/model.py | 2 +- .../tools/sceneinventory/models/containers.py | 131 +++++++++++------- client/ayon_core/tools/sceneinventory/view.py | 9 +- 5 files changed, 97 insertions(+), 56 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index ee2c1af07f..6f69651a8f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -465,7 +465,7 @@ def update_container(container, version=-1): from ayon_core.pipeline import get_current_project_name # Compute the different version from 'representation' - project_name = get_current_project_name() + project_name = container.get("project_name", get_current_project_name()) repre_id = container["representation"] if not _is_valid_representation_id(repre_id): raise ValueError( @@ -588,7 +588,7 @@ def switch_container(container, representation, loader_plugin=None): ) # Get the new representation to switch to - project_name = get_current_project_name() + project_name = container.get("project_name", get_current_project_name()) context = get_representation_context( project_name, representation["id"] diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index b890462506..8ce3a1bb7a 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -4,7 +4,7 @@ from ayon_core.lib.events import QueuedEventSystem from ayon_core.host import HostBase from ayon_core.pipeline import ( registered_host, - get_current_context, + get_current_context ) from ayon_core.tools.common_models import HierarchyModel, ProjectsModel @@ -110,8 +110,9 @@ class SceneInventoryController: representation_ids ) - def get_version_items(self, product_ids): - return self._containers_model.get_version_items(product_ids) + def get_version_items(self, product_ids, representation_ids): + return self._containers_model.get_version_items( + product_ids, representation_ids) # Site Sync methods def is_sitesync_enabled(self): diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index b7f79986ac..9d1202a906 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -150,7 +150,7 @@ class InventoryModel(QtGui.QStandardItemModel): if repre_info.is_valid } version_items_by_product_id = self._controller.get_version_items( - product_ids + product_ids, repre_id ) # SiteSync addon information progress_by_id = self._controller.get_representations_site_progress( diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 871455c96b..693a5948c9 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -6,6 +6,7 @@ from ayon_api.graphql import GraphQlQuery from ayon_core.host import ILoadHost from ayon_core.tools.common_models.projects import StatusStates +from ayon_core.pipeline.context_tools import get_current_project_name # --- Implementation that should be in ayon-python-api --- @@ -93,13 +94,15 @@ class ContainerItem: loader_name, namespace, object_name, - item_id + item_id, + project_name ): self.representation_id = representation_id self.loader_name = loader_name self.object_name = object_name self.namespace = namespace self.item_id = item_id + self.project_name = project_name @classmethod def from_container_data(cls, container): @@ -109,6 +112,8 @@ class ContainerItem: namespace=container["namespace"], object_name=container["objectName"], item_id=uuid.uuid4().hex, + project_name=container.get( + "project_name", get_current_project_name()) ) @@ -222,6 +227,9 @@ class ContainersModel: def get_representation_info_items(self, representation_ids): output = {} missing_repre_ids = set() + missing_repre_ids_by_project = {} + containers = self._controller.get_containers() + for repre_id in representation_ids: try: uuid.UUID(repre_id) @@ -229,54 +237,60 @@ class ContainersModel: output[repre_id] = RepresentationInfo.new_invalid() continue + project_name = self._find_project_name(containers, repre_id) + if project_name is None: + project_name = self._controller.get_current_project_name() + repre_info = self._repre_info_by_id.get(repre_id) if repre_info is None: missing_repre_ids.add(repre_id) + missing_repre_ids_by_project.update({project_name: repre_id}) else: output[repre_id] = repre_info if not missing_repre_ids: return output - project_name = self._controller.get_current_project_name() - repre_hierarchy_by_id = get_representations_hierarchy( - project_name, missing_repre_ids - ) - for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): - kwargs = { - "folder_id": None, - "folder_path": None, - "product_id": None, - "product_name": None, - "product_type": None, - "product_group": None, - "version_id": None, - "representation_name": None, - } - folder = repre_hierarchy.folder - product = repre_hierarchy.product - version = repre_hierarchy.version - repre = repre_hierarchy.representation - if folder: - kwargs["folder_id"] = folder["id"] - kwargs["folder_path"] = folder["path"] - if product: - group = product["attrib"]["productGroup"] - kwargs["product_id"] = product["id"] - kwargs["product_name"] = product["name"] - kwargs["product_type"] = product["productType"] - kwargs["product_group"] = group - if version: - kwargs["version_id"] = version["id"] - if repre: - kwargs["representation_name"] = repre["name"] + for project_name, missing_ids in missing_repre_ids_by_project.items(): + repre_hierarchy_by_id = get_representations_hierarchy( + project_name, {missing_ids} + ) + for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): + kwargs = { + "folder_id": None, + "folder_path": None, + "product_id": None, + "product_name": None, + "product_type": None, + "product_group": None, + "version_id": None, + "representation_name": None, + } + folder = repre_hierarchy.folder + product = repre_hierarchy.product + version = repre_hierarchy.version + repre = repre_hierarchy.representation + if folder: + kwargs["folder_id"] = folder["id"] + kwargs["folder_path"] = folder["path"] + if product: + group = product["attrib"]["productGroup"] + kwargs["product_id"] = product["id"] + kwargs["product_name"] = product["name"] + kwargs["product_type"] = product["productType"] + kwargs["product_group"] = group + if version: + kwargs["version_id"] = version["id"] + if repre: + kwargs["representation_name"] = repre["name"] - repre_info = RepresentationInfo(**kwargs) - self._repre_info_by_id[repre_id] = repre_info - output[repre_id] = repre_info + repre_info = RepresentationInfo(**kwargs) + self._repre_info_by_id[repre_id] = repre_info + output[repre_id] = repre_info return output - def get_version_items(self, product_ids): + def get_version_items(self, product_ids, representation_ids): + project_ids_by_project_names = {} if not product_ids: return {} @@ -293,20 +307,37 @@ class ContainersModel: def version_sorted(entity): return entity["version"] + containers = self.get_containers() + for repre_id in representation_ids: + project_name = self._find_project_name(containers, repre_id) + if project_name is None: + project_name = self._controller.get_current_project_name() + repre_hierarchy_by_id = get_representations_hierarchy( + project_name, {repre_id} + ) + product_ids_list = set() + for repre_hierarchy in repre_hierarchy_by_id.values(): + product = repre_hierarchy.product + product_id = product["id"] + if product_id not in missing_ids: + continue + product_ids_list.add(product_id) + project_ids_by_project_names.update({project_name: product_ids_list}) - project_name = self._controller.get_current_project_name() version_entities_by_product_id = { product_id: [] for product_id in missing_ids } - - version_entities = list(ayon_api.get_versions( - project_name, - product_ids=missing_ids, - fields={"id", "version", "productId", "status"} - )) - version_entities.sort(key=version_sorted) - for version_entity in version_entities: + version_entities_list = [] + for project_name, missing_product_ids in project_ids_by_project_names.items(): + version_entities = list(ayon_api.get_versions( + project_name, + product_ids=missing_product_ids, + fields={"id", "version", "productId", "status"} + )) + version_entities_list.extend(version_entities) + version_entities_list.sort(key=version_sorted) + for version_entity in version_entities_list: product_id = version_entity["productId"] version_entities_by_product_id[product_id].append( version_entity @@ -337,12 +368,18 @@ class ContainersModel: self._version_items_by_product_id[product_id] = ( version_items_by_id ) - return { product_id: dict(self._version_items_by_product_id[product_id]) for product_id in product_ids } + def _find_project_name(self, containers, representation_id): + # Function to find the project name by representation + for container in containers: + if container.get('representation') == representation_id: + return container.get('project_name', get_current_project_name()) + return None + def _update_cache(self): if self._items_cache is not None: return diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 22ba15fda8..5fc2113824 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -228,7 +228,10 @@ class SceneInventoryView(QtWidgets.QTreeView): return version_items_by_product_id = self._controller.get_version_items( - product_ids + product_ids, { + container_item.representation_id + for container_item in container_items_by_id.values() + } ) has_outdated = False has_loaded_hero_versions = False @@ -751,7 +754,7 @@ class SceneInventoryView(QtWidgets.QTreeView): active_version_id = active_repre_info.version_id active_product_id = active_repre_info.product_id version_items_by_product_id = self._controller.get_version_items( - product_ids + product_ids, repre_ids ) version_items = list( version_items_by_product_id[active_product_id].values() @@ -943,7 +946,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if repre_info.is_valid } version_items_by_product_id = self._controller.get_version_items( - product_ids + product_ids, repre_ids ) update_containers = [] From dcb838e1454523b3cbee00f5fde54fdc0d36fb58 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Nov 2024 19:17:00 +0800 Subject: [PATCH 238/546] resolve the project root during updating container --- client/ayon_core/pipeline/load/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index 6f69651a8f..a6c5f0ce1f 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -542,9 +542,6 @@ def update_container(container, version=-1): ) ) - path = get_representation_path(new_representation) - if not path or not os.path.exists(path): - raise ValueError("Path {} doesn't exist".format(path)) project_entity = ayon_api.get_project(project_name) context = { "project": project_entity, @@ -553,6 +550,9 @@ def update_container(container, version=-1): "version": new_version, "representation": new_representation, } + path = get_representation_path_from_context(context) + if not path or not os.path.exists(path): + raise ValueError("Path {} doesn't exist".format(path)) return Loader().update(container, context) From a7908a46e94cf22ca7b42fa5f2dab2657a3e8f13 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 11 Nov 2024 12:50:05 +0100 Subject: [PATCH 239/546] Update handling of missing otioReviewClips data - Handle case where otioReviewClips is missing by logging a message. --- client/ayon_core/plugins/publish/extract_otio_review.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 2c6472f8a4..b222c6efc3 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -74,7 +74,10 @@ class ExtractOTIOReview( # TODO: what if handles are different in `versionData`? handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] - otio_review_clips = instance.data["otioReviewClips"] + otio_review_clips = instance.data.get("otioReviewClips") + + if otio_review_clips is None: + self.log.info(f"Instance `{instance}` has no otioReviewClips") # add plugin wide attributes self.representation_files = [] From 3da898b3440b5fd9ba75887205e033cc834685a2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 11 Nov 2024 15:37:59 +0100 Subject: [PATCH 240/546] Update client/ayon_core/pipeline/publish/publish_plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/publish/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 6a2f4c0279..57215eff68 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -206,7 +206,7 @@ class AYONPyblishPluginMixin: return False families = [instance.product_type] - families.extend(instance.data.get("families", [])) + families.extend(instance.get("families", [])) for _ in pyblish.logic.plugins_by_families([cls], families): return True return False From a6729802dc6c0f1cd26dbf9447536d947797f906 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:58:47 +0100 Subject: [PATCH 241/546] make sure version combobox has no focus policy --- client/ayon_core/tools/loader/ui/products_delegates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 9753da37af..fba9b5b3ca 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -222,6 +222,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): editor = VersionComboBox(product_id, parent) editor.setProperty("itemId", item_id) + editor.setFocusPolicy(QtCore.Qt.NoFocus) editor.value_changed.connect(self._on_editor_change) editor.destroyed.connect(self._on_destroy) From a089b17f2ffc673830520d95976a57148905a965 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:20:21 +0100 Subject: [PATCH 242/546] added '__required_keys' to CreatedInstance --- client/ayon_core/pipeline/create/structures.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index ba4a373597..fdd41b7255 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -434,6 +434,13 @@ class CreatedInstance: "creator_attributes", "publish_attributes" ) + # Keys that can be changed, but should not be removed from instance + __required_keys = { + "folderPath": None, + "task": None, + "productName": None, + "active": True, + } def __init__( self, @@ -515,6 +522,9 @@ class CreatedInstance: if data: self._data.update(data) + for key, default in self.__required_keys.items(): + self._data.setdefault(key, default) + if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) @@ -567,6 +577,8 @@ class CreatedInstance: has_key = key in self._data output = self._data.pop(key, *args, **kwargs) if has_key: + if key in self.__required_keys: + self._data[key] = self.__required_keys[key] self._create_context.instance_values_changed( self.id, {key: None} ) From 2cf62f0bb455c401897f85f641774e70770ca1fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:20:35 +0100 Subject: [PATCH 243/546] fix product type key in immutable keys --- client/ayon_core/pipeline/create/structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index fdd41b7255..a1a4d5f8ef 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -429,7 +429,7 @@ class CreatedInstance: __immutable_keys = ( "id", "instance_id", - "product_type", + "productType", "creator_identifier", "creator_attributes", "publish_attributes" From b79e0189a073b579f9e45a0496ec6f87cbb3a617 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 Nov 2024 16:43:17 +0100 Subject: [PATCH 244/546] Use N/A label if is not available --- client/ayon_core/tools/publisher/models/create.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index ca26749b65..9644af43e0 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -296,7 +296,7 @@ class InstanceItem: return InstanceItem( instance.id, instance.creator_identifier, - instance.label, + instance.label or "N/A", instance.group_label, instance.product_type, instance.product_name, From 735409f9acb945c3af3cf61c9f2d35a2ce51de1e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 12 Nov 2024 20:13:49 +0800 Subject: [PATCH 245/546] do not get the container item from self.get_container --- .../ayon_core/tools/sceneinventory/model.py | 5 +- .../tools/sceneinventory/models/containers.py | 75 ++++++++----------- client/ayon_core/tools/sceneinventory/view.py | 22 +++--- 3 files changed, 48 insertions(+), 54 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 3857ea1700..687d130f04 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -130,6 +130,7 @@ class InventoryModel(QtGui.QStandardItemModel): self._clear_items() items_by_repre_id = {} + project_names = set() for container_item in container_items: # if ( # selected is not None @@ -137,8 +138,10 @@ class InventoryModel(QtGui.QStandardItemModel): # ): # continue repre_id = container_item.representation_id + project_name = container_item.project_name items = items_by_repre_id.setdefault(repre_id, []) items.append(container_item) + project_names.add(project_name) repre_id = set(items_by_repre_id.keys()) repre_info_by_id = self._controller.get_representation_info_items( @@ -150,7 +153,7 @@ class InventoryModel(QtGui.QStandardItemModel): if repre_info.is_valid } version_items_by_product_id = self._controller.get_version_items( - product_ids, repre_id + product_ids, project_names ) # SiteSync addon information progress_by_id = self._controller.get_representations_site_progress( diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 8a4beed52c..f5618d9f35 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -6,7 +6,6 @@ from ayon_api.graphql import GraphQlQuery from ayon_core.host import ILoadHost from ayon_core.tools.common_models.projects import StatusStates -from ayon_core.pipeline.context_tools import get_current_project_name # --- Implementation that should be in ayon-python-api --- @@ -112,8 +111,7 @@ class ContainerItem: namespace=container["namespace"], object_name=container["objectName"], item_id=uuid.uuid4().hex, - project_name=container.get( - "project_name", get_current_project_name()) + project_name=container.get("project_name", None) ) @@ -194,15 +192,21 @@ class ContainersModel: self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} + self._container_items_by_project = {} + self._project_name_by_repre_id = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} + self._product_id_by_project = {} def reset(self): self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} + self._container_items_by_project = {} + self._project_name_by_repre_id = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} + self._product_id_by_project = {} def get_containers(self): self._update_cache() @@ -226,10 +230,8 @@ class ContainersModel: def get_representation_info_items(self, representation_ids): output = {} - missing_repre_ids = set() missing_repre_ids_by_project = {} - containers = self._controller.get_containers() - + current_project_name = self._controller.get_current_project_name() for repre_id in representation_ids: try: uuid.UUID(repre_id) @@ -237,23 +239,23 @@ class ContainersModel: output[repre_id] = RepresentationInfo.new_invalid() continue - project_name = self._find_project_name(containers, repre_id) + project_name = self._project_name_by_repre_id.get(repre_id) if project_name is None: - project_name = self._controller.get_current_project_name() - + project_name = current_project_name repre_info = self._repre_info_by_id.get(repre_id) if repre_info is None: - missing_repre_ids.add(repre_id) - missing_repre_ids_by_project.update({project_name: repre_id}) + missing_repre_ids_by_project.setdefault( + project_name, set() + ).add(repre_id) else: output[repre_id] = repre_info - if not missing_repre_ids: + if not missing_repre_ids_by_project: return output for project_name, missing_ids in missing_repre_ids_by_project.items(): repre_hierarchy_by_id = get_representations_hierarchy( - project_name, {missing_ids} + project_name, missing_ids ) for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): kwargs = { @@ -286,19 +288,23 @@ class ContainersModel: repre_info = RepresentationInfo(**kwargs) self._repre_info_by_id[repre_id] = repre_info + self._product_id_by_project[project_name] = repre_info.product_id output[repre_id] = repre_info return output - def get_version_items(self, product_ids, representation_ids): - project_ids_by_project_names = {} + def get_version_items(self, product_ids, project_names): if not product_ids: return {} - missing_ids = { product_id for product_id in product_ids if product_id not in self._version_items_by_product_id } + + product_ids_by_project = { + project_name: self._product_id_by_project.get(project_name) + for project_name in project_names + } if missing_ids: status_items_by_name = { status_item.name: status_item @@ -307,34 +313,20 @@ class ContainersModel: def version_sorted(entity): return entity["version"] - containers = self.get_containers() - for repre_id in representation_ids: - project_name = self._find_project_name(containers, repre_id) - if project_name is None: - project_name = self._controller.get_current_project_name() - repre_hierarchy_by_id = get_representations_hierarchy( - project_name, {repre_id} - ) - product_ids_list = set() - for repre_hierarchy in repre_hierarchy_by_id.values(): - product = repre_hierarchy.product - product_id = product["id"] - if product_id not in missing_ids: - continue - product_ids_list.add(product_id) - project_ids_by_project_names.update({project_name: product_ids_list}) - + version_entities_list = [] version_entities_by_product_id = { product_id: [] for product_id in missing_ids } - version_entities_list = [] - for project_name, missing_product_ids in project_ids_by_project_names.items(): + for project_name, product_id in product_ids_by_project.items(): + if product_id not in missing_ids: + continue version_entities = list(ayon_api.get_versions( project_name, - product_ids=missing_product_ids, + product_ids={product_id}, fields={"id", "version", "productId", "status"} )) + version_entities_list.extend(version_entities) version_entities_list.sort(key=version_sorted) for version_entity in version_entities_list: @@ -342,7 +334,6 @@ class ContainersModel: version_entities_by_product_id[product_id].append( version_entity ) - for product_id, version_entities in ( version_entities_by_product_id.items() ): @@ -373,13 +364,6 @@ class ContainersModel: for product_id in product_ids } - def _find_project_name(self, containers, representation_id): - # Function to find the project name by representation - for container in containers: - if container.get('representation') == representation_id: - return container.get('project_name', get_current_project_name()) - return None - def _update_cache(self): if self._items_cache is not None: return @@ -395,6 +379,7 @@ class ContainersModel: container_items = [] containers_by_id = {} container_items_by_id = {} + project_name_by_repre_id = {} invalid_ids_mapping = {} for container in containers: try: @@ -418,8 +403,10 @@ class ContainersModel: containers_by_id[item.item_id] = container container_items_by_id[item.item_id] = item + project_name_by_repre_id[item.representation_id] = item.project_name container_items.append(item) self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id + self._project_name_by_repre_id = project_name_by_repre_id self._items_cache = container_items diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 5fc2113824..c5a25fa6dc 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -208,6 +208,7 @@ class SceneInventoryView(QtWidgets.QTreeView): filtered_items = [] product_ids = set() version_ids = set() + project_names = set() for container_item in container_items_by_id.values(): repre_id = container_item.representation_id repre_info = repre_info_by_id.get(repre_id) @@ -215,6 +216,7 @@ class SceneInventoryView(QtWidgets.QTreeView): filtered_items.append(container_item) version_ids.add(repre_info.version_id) product_ids.add(repre_info.product_id) + project_names.add(container_item.project_name) # remove remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) @@ -228,11 +230,7 @@ class SceneInventoryView(QtWidgets.QTreeView): return version_items_by_product_id = self._controller.get_version_items( - product_ids, { - container_item.representation_id - for container_item in container_items_by_id.values() - } - ) + product_ids, project_names) has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False @@ -742,6 +740,10 @@ class SceneInventoryView(QtWidgets.QTreeView): container_item.representation_id for container_item in container_items_by_id.values() } + project_names = { + container_item.project_name + for container_item in container_items_by_id.values() + } repre_info_by_id = self._controller.get_representation_info_items( repre_ids ) @@ -754,8 +756,7 @@ class SceneInventoryView(QtWidgets.QTreeView): active_version_id = active_repre_info.version_id active_product_id = active_repre_info.product_id version_items_by_product_id = self._controller.get_version_items( - product_ids, repre_ids - ) + product_ids, project_names) version_items = list( version_items_by_product_id[active_product_id].values() ) @@ -937,6 +938,10 @@ class SceneInventoryView(QtWidgets.QTreeView): container_item.representation_id for container_item in containers_items_by_id.values() } + project_names = { + container_item.project_name + for container_item in containers_items_by_id.values() + } repre_info_by_id = self._controller.get_representation_info_items( repre_ids ) @@ -946,8 +951,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if repre_info.is_valid } version_items_by_product_id = self._controller.get_version_items( - product_ids, repre_ids - ) + product_ids, project_names) update_containers = [] update_versions = [] From 235949b867aff1f5caf5139e60ec4ca136ae2d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 12 Nov 2024 15:58:08 +0100 Subject: [PATCH 246/546] Update client/ayon_core/plugins/publish/collect_otio_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_otio_review.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 04422391c5..4708b0a97c 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -100,37 +100,33 @@ class CollectOtioReview(pyblish.api.InstancePlugin): "Creating review track: {}".format(otio_review_clips)) # get colorspace from metadata if available - if len(otio_review_clips) >= 1 and any( - # lets make sure any clip with media reference is found + # get metadata from first clip with media reference + r_otio_cl = next( ( clip for clip in otio_review_clips - if isinstance(clip, otio.schema.Clip) - and clip.media_reference - ) - ): - # get metadata from first clip - # get colorspace from metadata if available - # check if resolution is the same as source - r_otio_cl = next( - ( - clip - for clip in otio_review_clips - if isinstance(clip, otio.schema.Clip) + if ( + isinstance(clip, otio.schema.Clip) and clip.media_reference - ), - None, - ) - - # get metadata from first clip with media reference + ) + ), + None + ) + if r_otio_cl is not None: media_ref = r_otio_cl.media_reference media_metadata = media_ref.metadata # TODO: we might need some alternative method since # native OTIO exports do not support ayon metadata - if review_colorspace := media_metadata.get( + review_colorspace = media_metadata.get( "ayon.source.colorspace" - ): + ) + if review_colorspace is None: + # Backwards compatibility for older scenes + review_colorspace = media_metadata.get( + "openpype.source.colourtransform" + ) + if review_colorspace: instance.data["reviewColorspace"] = review_colorspace self.log.info( "Review colorspace: {}".format(review_colorspace)) From 863c6f51871f089acfba690ee58ac4af95a14a21 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 12 Nov 2024 16:04:25 -0500 Subject: [PATCH 247/546] Allow CSV ingest to create new shots. --- .../plugins/publish/collect_hierarchy.py | 18 +++++++++++------- .../publish/extract_hierarchy_to_ayon.py | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 2ae3cc67f3..e4b4dd408f 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -13,8 +13,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): label = "Collect Hierarchy" order = pyblish.api.CollectorOrder - 0.076 - families = ["shot"] - hosts = ["resolve", "hiero", "flame"] + families = ["shot", "csv_ingest_shot"] + hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, context): project_name = context.data["projectName"] @@ -38,8 +38,9 @@ class CollectHierarchy(pyblish.api.ContextPlugin): ): continue - # exclude if not masterLayer True - if not instance.data.get("heroTrack"): + # exclude if not CSV ingest shot and not masterLayer True + if ("csv_ingest_shot" not in families and + not instance.data.get("heroTrack")): continue shot_data = { @@ -49,7 +50,10 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "folder_type": "Shot", "tasks": instance.data.get("tasks") or {}, "comments": instance.data.get("comments", []), - "attributes": { + } + + if "csv_ingest_shot" not in families: + shot_data["attributes"] = { "handleStart": instance.data["handleStart"], "handleEnd": instance.data["handleEnd"], "frameStart": instance.data["frameStart"], @@ -60,8 +64,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "resolutionWidth": instance.data["resolutionWidth"], "resolutionHeight": instance.data["resolutionHeight"], "pixelAspect": instance.data["pixelAspect"], - }, - } + } + # Split by '/' for AYON where asset is a path name = instance.data["folderPath"].split("/")[-1] actual = {name: shot_data} diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index a169affc66..390ce36126 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -22,7 +22,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract Hierarchy To AYON" - families = ["clip", "shot"] + families = ["clip", "shot", "csv_ingest_shot"] def process(self, context): if not context.data.get("hierarchyContext"): From f6547264fbcbd293036a4e23059e2774114c441c Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 12 Nov 2024 16:30:55 -0500 Subject: [PATCH 248/546] Fix lint. --- client/ayon_core/plugins/publish/collect_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index e4b4dd408f..3340430345 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -64,7 +64,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "resolutionWidth": instance.data["resolutionWidth"], "resolutionHeight": instance.data["resolutionHeight"], "pixelAspect": instance.data["pixelAspect"], - } + } # Split by '/' for AYON where asset is a path name = instance.data["folderPath"].split("/")[-1] From bccd8d813c7b22c57abd3b8b25ad6d19e55a6911 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Nov 2024 10:51:16 +0000 Subject: [PATCH 249/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 3a5b63785d..7702eb67ad 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.7+dev" +__version__ = "1.0.8" diff --git a/package.py b/package.py index ef2f3822eb..bd61438898 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.7+dev" +version = "1.0.8" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 78a3021b30..236a7ddc6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.7+dev" +version = "1.0.8" description = "" authors = ["Ynput Team "] readme = "README.md" From a181fc897d16db9563fca73473eddee590cdd427 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Nov 2024 10:51:57 +0000 Subject: [PATCH 250/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 7702eb67ad..63f7de04dc 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.8" +__version__ = "1.0.8+dev" diff --git a/package.py b/package.py index bd61438898..bbfcc51019 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.8" +version = "1.0.8+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 236a7ddc6c..e29aa08c6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.8" +version = "1.0.8+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 0bec953dec2d786afb09a420e86f83980ddb19a1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Nov 2024 22:46:43 +0800 Subject: [PATCH 251/546] query the product id per project --- .../ayon_core/tools/sceneinventory/control.py | 4 +- .../ayon_core/tools/sceneinventory/model.py | 21 +++-- .../tools/sceneinventory/models/containers.py | 30 +++---- client/ayon_core/tools/sceneinventory/view.py | 86 ++++++++++++------- 4 files changed, 85 insertions(+), 56 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 8ce3a1bb7a..640911df80 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -110,9 +110,9 @@ class SceneInventoryController: representation_ids ) - def get_version_items(self, product_ids, representation_ids): + def get_version_items(self, project_name, product_ids): return self._containers_model.get_version_items( - product_ids, representation_ids) + project_name, product_ids) # Site Sync methods def is_sitesync_enabled(self): diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 687d130f04..162a0d4b71 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -152,9 +152,20 @@ class InventoryModel(QtGui.QStandardItemModel): for repre_info in repre_info_by_id.values() if repre_info.is_valid } - version_items_by_product_id = self._controller.get_version_items( - product_ids, project_names - ) + + project_products = {project_name: set() for project_name in project_names} + for representation_id, items in items_by_repre_id.items(): + repre_info = repre_info_by_id.get(representation_id) + if repre_info and repre_info.is_valid: + product_id = repre_info.product_id + for item in items: + project_name = item.project_name + project_products[project_name].add(product_id) + version_items_by_product_id = {} + for project_name, product_ids in project_products.items(): + version_items_by_product_id.update(self._controller.get_version_items( + project_name, product_ids + )) # SiteSync addon information progress_by_id = self._controller.get_representations_site_progress( repre_id @@ -236,7 +247,6 @@ class InventoryModel(QtGui.QStandardItemModel): for container_item in container_items: object_name = container_item.object_name or "" unique_name = repre_name + object_name - item = QtGui.QStandardItem() item.setColumnCount(root_item.columnCount()) item.setData(container_item.namespace, QtCore.Qt.DisplayRole) @@ -251,7 +261,6 @@ class InventoryModel(QtGui.QStandardItemModel): item.setData(True, IS_CONTAINER_ITEM_ROLE) item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) container_model_items.append(item) - if not container_model_items: continue @@ -290,7 +299,7 @@ class InventoryModel(QtGui.QStandardItemModel): group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE) group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE) group_item.setData(False, IS_CONTAINER_ITEM_ROLE) - + print(group_item) if version_color is not None: group_item.setData(version_color, VERSION_COLOR_ROLE) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index f5618d9f35..4592b489e1 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -249,7 +249,6 @@ class ContainersModel: ).add(repre_id) else: output[repre_id] = repre_info - if not missing_repre_ids_by_project: return output @@ -292,7 +291,7 @@ class ContainersModel: output[repre_id] = repre_info return output - def get_version_items(self, product_ids, project_names): + def get_version_items(self, project_name, product_ids): if not product_ids: return {} missing_ids = { @@ -301,10 +300,7 @@ class ContainersModel: if product_id not in self._version_items_by_product_id } - product_ids_by_project = { - project_name: self._product_id_by_project.get(project_name) - for project_name in project_names - } + current_product_id = self._product_id_by_project.get(project_name) if missing_ids: status_items_by_name = { status_item.name: status_item @@ -313,24 +309,22 @@ class ContainersModel: def version_sorted(entity): return entity["version"] - version_entities_list = [] + if current_product_id not in missing_ids: + return version_entities_by_product_id = { product_id: [] for product_id in missing_ids } - for project_name, product_id in product_ids_by_project.items(): + version_entities = list(ayon_api.get_versions( + project_name, + product_ids={current_product_id}, + fields={"id", "version", "productId", "status"} + )) + version_entities.sort(key=version_sorted) + for version_entity in version_entities: + product_id = version_entity["productId"] if product_id not in missing_ids: continue - version_entities = list(ayon_api.get_versions( - project_name, - product_ids={product_id}, - fields={"id", "version", "productId", "status"} - )) - - version_entities_list.extend(version_entities) - version_entities_list.sort(key=version_sorted) - for version_entity in version_entities_list: - product_id = version_entity["productId"] version_entities_by_product_id[product_id].append( version_entity ) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index c5a25fa6dc..12a7ab2285 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -206,18 +206,20 @@ class SceneInventoryView(QtWidgets.QTreeView): # Exclude items that are "NOT FOUND" since setting versions, updating # and removal won't work for those items. filtered_items = [] - product_ids = set() + project_products = {} version_ids = set() - project_names = set() for container_item in container_items_by_id.values(): repre_id = container_item.representation_id + project_name = container_item.project_name repre_info = repre_info_by_id.get(repre_id) if repre_info and repre_info.is_valid: filtered_items.append(container_item) version_ids.add(repre_info.version_id) - product_ids.add(repre_info.product_id) - project_names.add(container_item.project_name) - + product_id = repre_info.product_id + if project_name not in project_products: + project_products[project_name] = set() + project_products[project_name].add(product_id) + print("p_products", project_products) # remove remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) @@ -228,9 +230,12 @@ class SceneInventoryView(QtWidgets.QTreeView): # Keep remove action for invalid items menu.addAction(remove_action) return - - version_items_by_product_id = self._controller.get_version_items( - product_ids, project_names) + version_items_by_product_id = {} + for project_name, product_ids in project_products.items(): + version_items_by_product_id.update( + self._controller.get_version_items( + project_name, product_ids) + ) has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False @@ -736,14 +741,11 @@ class SceneInventoryView(QtWidgets.QTreeView): container_items_by_id = self._controller.get_container_items_by_id( item_ids ) + print(container_items_by_id, "container") repre_ids = { container_item.representation_id for container_item in container_items_by_id.values() } - project_names = { - container_item.project_name - for container_item in container_items_by_id.values() - } repre_info_by_id = self._controller.get_representation_info_items( repre_ids ) @@ -752,11 +754,25 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_info.product_id for repre_info in repre_info_by_id.values() } + project_products = {} + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + project_name = container_item.project_name + repre_info = repre_info_by_id.get(repre_id) + if repre_info and repre_info.is_valid: + if project_name not in project_products: + project_products[project_name] = set() + product_id = repre_info.product_id + project_products[project_name].add(product_id) + print("proj_product", project_products) active_repre_info = repre_info_by_id[active_repre_id] active_version_id = active_repre_info.version_id active_product_id = active_repre_info.product_id - version_items_by_product_id = self._controller.get_version_items( - product_ids, project_names) + version_items_by_product_id = {} + for project_name, product_ids in project_products.items(): + version_items_by_product_id.update( + self._controller.get_version_items( + project_name, product_ids)) version_items = list( version_items_by_product_id[active_product_id].values() ) @@ -931,27 +947,37 @@ class SceneInventoryView(QtWidgets.QTreeView): self._update_containers_to_version(item_ids, version=-1) def _on_switch_to_versioned(self, item_ids): - containers_items_by_id = self._controller.get_container_items_by_id( - item_ids - ) + # Get container items by ID + containers_items_by_id = self._controller.get_container_items_by_id(item_ids) repre_ids = { container_item.representation_id for container_item in containers_items_by_id.values() } - project_names = { - container_item.project_name - for container_item in containers_items_by_id.values() + # Extract project names and their corresponding representation IDs + project_name_to_repre_ids = {} + for container_item in containers_items_by_id.values(): + project_name = container_item.project_name + repre_id = container_item.representation_id + if project_name not in project_name_to_repre_ids: + project_name_to_repre_ids[project_name] = set() + project_name_to_repre_ids[project_name].add(repre_id) + + # Get representation info items by ID + repre_info_by_id = self._controller.get_representation_info_items(repre_ids) + + # Create a dictionary to map project names to sets of product IDs + project_products = { + project_name: set() for project_name in project_name_to_repre_ids.keys() } - repre_info_by_id = self._controller.get_representation_info_items( - repre_ids - ) - product_ids = { - repre_info.product_id - for repre_info in repre_info_by_id.values() - if repre_info.is_valid - } - version_items_by_product_id = self._controller.get_version_items( - product_ids, project_names) + + print("project_products", project_products) + version_items_by_product_id = {} + for project_name, product_ids in project_name_to_repre_ids.items(): + version_items_by_product_id.update( + self._controller.get_version_items( + project_name, product_ids + ) + ) update_containers = [] update_versions = [] From 574ea3580da0ad9531c77e9297660c35e5e20f5b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Nov 2024 21:58:59 +0800 Subject: [PATCH 252/546] codes clean up & make sure it supports mulitple asset loading per project --- .../ayon_core/tools/sceneinventory/control.py | 4 +- .../ayon_core/tools/sceneinventory/model.py | 45 +++--- .../tools/sceneinventory/models/containers.py | 34 ++--- client/ayon_core/tools/sceneinventory/view.py | 138 +++++++++++------- 4 files changed, 127 insertions(+), 94 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 640911df80..310e41b117 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -105,9 +105,9 @@ class SceneInventoryController: def get_container_items_by_id(self, item_ids): return self._containers_model.get_container_items_by_id(item_ids) - def get_representation_info_items(self, representation_ids): + def get_representation_info_items(self, project_name, representation_ids): return self._containers_model.get_representation_info_items( - representation_ids + project_name, representation_ids ) def get_version_items(self, project_name, product_ids): diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 162a0d4b71..a37e3a2b40 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -129,43 +129,54 @@ class InventoryModel(QtGui.QStandardItemModel): self._clear_items() - items_by_repre_id = {} - project_names = set() + items_by_repre_id = collections.defaultdict(list) + repre_ids_by_project = collections.defaultdict(set) for container_item in container_items: # if ( # selected is not None # and container_item.item_id not in selected # ): # continue + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) repre_id = container_item.representation_id - project_name = container_item.project_name - items = items_by_repre_id.setdefault(repre_id, []) - items.append(container_item) - project_names.add(project_name) + items_by_repre_id[repre_id].append(container_item) + repre_ids_by_project[project_name].add(repre_id) repre_id = set(items_by_repre_id.keys()) - repre_info_by_id = self._controller.get_representation_info_items( - repre_id - ) + repre_info_by_id = {} + for project_name, repre_ids in repre_ids_by_project.items(): + repre_info = self._controller.get_representation_info_items( + project_name, repre_ids + ) + repre_info_by_id.update(repre_info) product_ids = { repre_info.product_id for repre_info in repre_info_by_id.values() if repre_info.is_valid } - project_products = {project_name: set() for project_name in project_names} - for representation_id, items in items_by_repre_id.items(): + project_products = collections.defaultdict(set) + for container_item in container_items: + representation_id = container_item.representation_id + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) repre_info = repre_info_by_id.get(representation_id) if repre_info and repre_info.is_valid: product_id = repre_info.product_id - for item in items: - project_name = item.project_name - project_products[project_name].add(product_id) + project_products[project_name].add(product_id) + version_items_by_product_id = {} for project_name, product_ids in project_products.items(): - version_items_by_product_id.update(self._controller.get_version_items( + version_items = self._controller.get_version_items( project_name, product_ids - )) + ) + version_items_by_product_id.update(version_items) + # SiteSync addon information progress_by_id = self._controller.get_representations_site_progress( repre_id @@ -299,7 +310,7 @@ class InventoryModel(QtGui.QStandardItemModel): group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE) group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE) group_item.setData(False, IS_CONTAINER_ITEM_ROLE) - print(group_item) + if version_color is not None: group_item.setData(version_color, VERSION_COLOR_ROLE) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 4592b489e1..52b568af03 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -193,20 +193,18 @@ class ContainersModel: self._containers_by_id = {} self._container_items_by_id = {} self._container_items_by_project = {} - self._project_name_by_repre_id = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} - self._product_id_by_project = {} + self._product_ids_by_project = {} def reset(self): self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} self._container_items_by_project = {} - self._project_name_by_repre_id = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} - self._product_id_by_project = {} + self._product_ids_by_project = {} def get_containers(self): self._update_cache() @@ -228,20 +226,17 @@ class ContainersModel: for item_id in item_ids } - def get_representation_info_items(self, representation_ids): + def get_representation_info_items(self, project_name, representation_ids): output = {} missing_repre_ids_by_project = {} - current_project_name = self._controller.get_current_project_name() + if project_name is None: + project_name = self._controller.get_current_project_name() for repre_id in representation_ids: try: uuid.UUID(repre_id) except ValueError: output[repre_id] = RepresentationInfo.new_invalid() continue - - project_name = self._project_name_by_repre_id.get(repre_id) - if project_name is None: - project_name = current_project_name repre_info = self._repre_info_by_id.get(repre_id) if repre_info is None: missing_repre_ids_by_project.setdefault( @@ -256,6 +251,7 @@ class ContainersModel: repre_hierarchy_by_id = get_representations_hierarchy( project_name, missing_ids ) + self._product_ids_by_project[project_name] = set() for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): kwargs = { "folder_id": None, @@ -287,20 +283,22 @@ class ContainersModel: repre_info = RepresentationInfo(**kwargs) self._repre_info_by_id[repre_id] = repre_info - self._product_id_by_project[project_name] = repre_info.product_id + self._product_ids_by_project[project_name].add( + repre_info.product_id) output[repre_id] = repre_info return output def get_version_items(self, project_name, product_ids): if not product_ids: return {} + if project_name is None: + project_name = self._controller.get_current_project_name() missing_ids = { product_id for product_id in product_ids if product_id not in self._version_items_by_product_id } - - current_product_id = self._product_id_by_project.get(project_name) + current_product_ids = self._product_ids_by_project.get(project_name) if missing_ids: status_items_by_name = { status_item.name: status_item @@ -309,22 +307,19 @@ class ContainersModel: def version_sorted(entity): return entity["version"] - if current_product_id not in missing_ids: - return + current_missing_ids = current_product_ids.intersection(missing_ids) version_entities_by_product_id = { product_id: [] - for product_id in missing_ids + for product_id in current_missing_ids } version_entities = list(ayon_api.get_versions( project_name, - product_ids={current_product_id}, + product_ids=current_missing_ids, fields={"id", "version", "productId", "status"} )) version_entities.sort(key=version_sorted) for version_entity in version_entities: product_id = version_entity["productId"] - if product_id not in missing_ids: - continue version_entities_by_product_id[product_id].append( version_entity ) @@ -402,5 +397,4 @@ class ContainersModel: self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id - self._project_name_by_repre_id = project_name_by_repre_id self._items_cache = container_items diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 12a7ab2285..1e0b96570e 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -192,11 +192,20 @@ class SceneInventoryView(QtWidgets.QTreeView): container_item = container_items_by_id[item_id] active_repre_id = container_item.representation_id break + repre_ids_by_project = collections.defaultdict(set) + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) + repre_ids_by_project[project_name].add(repre_id) + repre_info_by_id = {} + for project_name, repre_ids in repre_ids_by_project.items(): + repre_info = self._controller.get_representation_info_items( + project_name, repre_ids) + repre_info_by_id.update(repre_info) - repre_info_by_id = self._controller.get_representation_info_items({ - container_item.representation_id - for container_item in container_items_by_id.values() - }) valid_repre_ids = { repre_id for repre_id, repre_info in repre_info_by_id.items() @@ -206,20 +215,20 @@ class SceneInventoryView(QtWidgets.QTreeView): # Exclude items that are "NOT FOUND" since setting versions, updating # and removal won't work for those items. filtered_items = [] - project_products = {} + product_ids_by_project = collections.defaultdict(set) version_ids = set() for container_item in container_items_by_id.values(): repre_id = container_item.representation_id - project_name = container_item.project_name + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) repre_info = repre_info_by_id.get(repre_id) if repre_info and repre_info.is_valid: filtered_items.append(container_item) version_ids.add(repre_info.version_id) product_id = repre_info.product_id - if project_name not in project_products: - project_products[project_name] = set() - project_products[project_name].add(product_id) - print("p_products", project_products) + product_ids_by_project[project_name].add(product_id) # remove remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) @@ -231,11 +240,12 @@ class SceneInventoryView(QtWidgets.QTreeView): menu.addAction(remove_action) return version_items_by_product_id = {} - for project_name, product_ids in project_products.items(): - version_items_by_product_id.update( - self._controller.get_version_items( - project_name, product_ids) + for project_name, product_ids in product_ids_by_project.items(): + version_items = self._controller.get_version_items( + project_name, product_ids ) + version_items_by_product_id.update(version_items +) has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False @@ -741,38 +751,47 @@ class SceneInventoryView(QtWidgets.QTreeView): container_items_by_id = self._controller.get_container_items_by_id( item_ids ) - print(container_items_by_id, "container") - repre_ids = { - container_item.representation_id - for container_item in container_items_by_id.values() - } - repre_info_by_id = self._controller.get_representation_info_items( - repre_ids - ) + repre_ids_by_project = collections.defaultdict(set) + for container_item in container_items_by_id.values(): + repre_id = container_item.representation_id + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) + repre_ids_by_project[project_name].add(repre_id) + repre_info_by_id = {} + for project_name, repre_ids in repre_ids_by_project.items(): + repre_info = self._controller.get_representation_info_items( + project_name, repre_ids + ) + repre_info_by_id.update(repre_info) product_ids = { repre_info.product_id for repre_info in repre_info_by_id.values() } - project_products = {} + product_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): repre_id = container_item.representation_id - project_name = container_item.project_name + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) repre_info = repre_info_by_id.get(repre_id) - if repre_info and repre_info.is_valid: - if project_name not in project_products: - project_products[project_name] = set() - product_id = repre_info.product_id - project_products[project_name].add(product_id) - print("proj_product", project_products) + if not repre_info or not repre_info.is_valid: + continue + product_ids_by_project[project_name].add( + repre_info.product_id + ) active_repre_info = repre_info_by_id[active_repre_id] active_version_id = active_repre_info.version_id active_product_id = active_repre_info.product_id version_items_by_product_id = {} - for project_name, product_ids in project_products.items(): - version_items_by_product_id.update( - self._controller.get_version_items( - project_name, product_ids)) + for project_name, project_product_ids in product_ids_by_project.items(): + version_items = self._controller.get_version_items( + project_name, project_product_ids + ) + version_items_by_product_id.update(version_items) version_items = list( version_items_by_product_id[active_product_id].values() ) @@ -949,35 +968,44 @@ class SceneInventoryView(QtWidgets.QTreeView): def _on_switch_to_versioned(self, item_ids): # Get container items by ID containers_items_by_id = self._controller.get_container_items_by_id(item_ids) - repre_ids = { - container_item.representation_id - for container_item in containers_items_by_id.values() - } # Extract project names and their corresponding representation IDs - project_name_to_repre_ids = {} + repre_ids_by_project = collections.defaultdict(set) for container_item in containers_items_by_id.values(): project_name = container_item.project_name + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() + ) repre_id = container_item.representation_id - if project_name not in project_name_to_repre_ids: - project_name_to_repre_ids[project_name] = set() - project_name_to_repre_ids[project_name].add(repre_id) + repre_ids_by_project[project_name].add(repre_id) # Get representation info items by ID - repre_info_by_id = self._controller.get_representation_info_items(repre_ids) + repre_info_by_id = {} + for project_name, repre_ids in repre_ids_by_project.items(): + repre_info = self._controller.get_representation_info_items( + project_name, repre_ids) + repre_info_by_id.update(repre_info) - # Create a dictionary to map project names to sets of product IDs - project_products = { - project_name: set() for project_name in project_name_to_repre_ids.keys() - } - - print("project_products", project_products) - version_items_by_product_id = {} - for project_name, product_ids in project_name_to_repre_ids.items(): - version_items_by_product_id.update( - self._controller.get_version_items( - project_name, product_ids - ) + product_ids_by_project = collections.defaultdict(set) + for container_item in containers_items_by_id.values(): + repre_id = container_item.representation_id + project_name = ( + container_item.project_name or + self._controller.get_current_project_name() ) + repre_info = repre_info_by_id.get(repre_id) + if not repre_info or not repre_info.is_valid: + continue + product_ids_by_project[project_name].add( + repre_info.product_id + ) + + version_items_by_product_id = {} + for project_name, product_ids in product_ids_by_project.items(): + version_items = self._controller.get_version_items( + project_name, product_ids + ) + version_items_by_product_id.update(version_items) update_containers = [] update_versions = [] From 8965a8859435473c0171053cc2908dd011c05ad8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 18 Nov 2024 17:59:20 +0800 Subject: [PATCH 253/546] clean up code and add project name row into scene inventory --- .../ayon_core/tools/sceneinventory/model.py | 8 +- .../tools/sceneinventory/models/containers.py | 87 +++++++++---------- 2 files changed, 47 insertions(+), 48 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index a37e3a2b40..03627e60b9 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -36,6 +36,7 @@ REMOTE_SITE_ICON_ROLE = QtCore.Qt.UserRole + 23 # This value hold unique value of container that should be used to identify # containers inbetween refresh. ITEM_UNIQUE_NAME_ROLE = QtCore.Qt.UserRole + 24 +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 25 class InventoryModel(QtGui.QStandardItemModel): @@ -52,6 +53,7 @@ class InventoryModel(QtGui.QStandardItemModel): "Object name", "Active site", "Remote site", + "Project" ] name_col = column_labels.index("Name") version_col = column_labels.index("Version") @@ -63,6 +65,7 @@ class InventoryModel(QtGui.QStandardItemModel): object_name_col = column_labels.index("Object name") active_site_col = column_labels.index("Active site") remote_site_col = column_labels.index("Remote site") + project_col = column_labels.index("Project") display_role_by_column = { name_col: QtCore.Qt.DisplayRole, version_col: VERSION_LABEL_ROLE, @@ -72,6 +75,7 @@ class InventoryModel(QtGui.QStandardItemModel): product_group_col: PRODUCT_GROUP_NAME_ROLE, loader_col: LOADER_NAME_ROLE, object_name_col: OBJECT_NAME_ROLE, + project_col: PROJECT_NAME_ROLE, active_site_col: ACTIVE_SITE_PROGRESS_ROLE, remote_site_col: REMOTE_SITE_PROGRESS_ROLE, } @@ -85,7 +89,7 @@ class InventoryModel(QtGui.QStandardItemModel): foreground_role_by_column = { name_col: NAME_COLOR_ROLE, version_col: VERSION_COLOR_ROLE, - status_col: STATUS_COLOR_ROLE + status_col: STATUS_COLOR_ROLE, } width_by_column = { name_col: 250, @@ -95,6 +99,7 @@ class InventoryModel(QtGui.QStandardItemModel): product_type_col: 150, product_group_col: 120, loader_col: 150, + project_col: 150, } OUTDATED_COLOR = QtGui.QColor(235, 30, 30) @@ -269,6 +274,7 @@ class InventoryModel(QtGui.QStandardItemModel): item.setData(version_label, VERSION_LABEL_ROLE) item.setData(container_item.loader_name, LOADER_NAME_ROLE) item.setData(container_item.object_name, OBJECT_NAME_ROLE) + item.setData(container_item.project_name, PROJECT_NAME_ROLE) item.setData(True, IS_CONTAINER_ITEM_ROLE) item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) container_model_items.append(item) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 52b568af03..b8b9aa400a 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -228,9 +228,7 @@ class ContainersModel: def get_representation_info_items(self, project_name, representation_ids): output = {} - missing_repre_ids_by_project = {} - if project_name is None: - project_name = self._controller.get_current_project_name() + missing_repre_ids = set() for repre_id in representation_ids: try: uuid.UUID(repre_id) @@ -239,60 +237,55 @@ class ContainersModel: continue repre_info = self._repre_info_by_id.get(repre_id) if repre_info is None: - missing_repre_ids_by_project.setdefault( - project_name, set() - ).add(repre_id) + missing_repre_ids.add(repre_id) else: output[repre_id] = repre_info - if not missing_repre_ids_by_project: + if not missing_repre_ids: return output - for project_name, missing_ids in missing_repre_ids_by_project.items(): - repre_hierarchy_by_id = get_representations_hierarchy( - project_name, missing_ids - ) - self._product_ids_by_project[project_name] = set() - for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): - kwargs = { - "folder_id": None, - "folder_path": None, - "product_id": None, - "product_name": None, - "product_type": None, - "product_group": None, - "version_id": None, - "representation_name": None, - } - folder = repre_hierarchy.folder - product = repre_hierarchy.product - version = repre_hierarchy.version - repre = repre_hierarchy.representation - if folder: - kwargs["folder_id"] = folder["id"] - kwargs["folder_path"] = folder["path"] - if product: - group = product["attrib"]["productGroup"] - kwargs["product_id"] = product["id"] - kwargs["product_name"] = product["name"] - kwargs["product_type"] = product["productType"] - kwargs["product_group"] = group - if version: - kwargs["version_id"] = version["id"] - if repre: - kwargs["representation_name"] = repre["name"] + repre_hierarchy_by_id = get_representations_hierarchy( + project_name, missing_repre_ids + ) + self._product_ids_by_project[project_name] = set() + for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): + kwargs = { + "folder_id": None, + "folder_path": None, + "product_id": None, + "product_name": None, + "product_type": None, + "product_group": None, + "version_id": None, + "representation_name": None, + } + folder = repre_hierarchy.folder + product = repre_hierarchy.product + version = repre_hierarchy.version + repre = repre_hierarchy.representation + if folder: + kwargs["folder_id"] = folder["id"] + kwargs["folder_path"] = folder["path"] + if product: + group = product["attrib"]["productGroup"] + kwargs["product_id"] = product["id"] + kwargs["product_name"] = product["name"] + kwargs["product_type"] = product["productType"] + kwargs["product_group"] = group + if version: + kwargs["version_id"] = version["id"] + if repre: + kwargs["representation_name"] = repre["name"] - repre_info = RepresentationInfo(**kwargs) - self._repre_info_by_id[repre_id] = repre_info - self._product_ids_by_project[project_name].add( - repre_info.product_id) - output[repre_id] = repre_info + repre_info = RepresentationInfo(**kwargs) + self._repre_info_by_id[repre_id] = repre_info + self._product_ids_by_project[project_name].add( + repre_info.product_id) + output[repre_id] = repre_info return output def get_version_items(self, project_name, product_ids): if not product_ids: return {} - if project_name is None: - project_name = self._controller.get_current_project_name() missing_ids = { product_id for product_id in product_ids From 0866c002019834e6a4aeda22becb9e5c208f4515 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 18 Nov 2024 19:18:50 +0800 Subject: [PATCH 254/546] code tweaks - kuba's comment --- .../ayon_core/tools/sceneinventory/model.py | 12 ++------ .../tools/sceneinventory/models/containers.py | 5 +++- client/ayon_core/tools/sceneinventory/view.py | 29 ++++--------------- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 03627e60b9..5f9e6bb77c 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -53,7 +53,7 @@ class InventoryModel(QtGui.QStandardItemModel): "Object name", "Active site", "Remote site", - "Project" + "Project", ] name_col = column_labels.index("Name") version_col = column_labels.index("Version") @@ -142,10 +142,7 @@ class InventoryModel(QtGui.QStandardItemModel): # and container_item.item_id not in selected # ): # continue - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_id = container_item.representation_id items_by_repre_id[repre_id].append(container_item) repre_ids_by_project[project_name].add(repre_id) @@ -166,10 +163,7 @@ class InventoryModel(QtGui.QStandardItemModel): project_products = collections.defaultdict(set) for container_item in container_items: representation_id = container_item.representation_id - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_info = repre_info_by_id.get(representation_id) if repre_info and repre_info.is_valid: product_id = repre_info.product_id diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index b8b9aa400a..c12a05fd99 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -5,6 +5,7 @@ import ayon_api from ayon_api.graphql import GraphQlQuery from ayon_core.host import ILoadHost +from ayon_core.pipeline import get_current_project_name from ayon_core.tools.common_models.projects import StatusStates @@ -111,7 +112,9 @@ class ContainerItem: namespace=container["namespace"], object_name=container["objectName"], item_id=uuid.uuid4().hex, - project_name=container.get("project_name", None) + project_name=container.get( + "project_name", get_current_project_name() + ) ) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 1e0b96570e..a049fd1e0b 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -195,10 +195,7 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): repre_id = container_item.representation_id - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_ids_by_project[project_name].add(repre_id) repre_info_by_id = {} for project_name, repre_ids in repre_ids_by_project.items(): @@ -219,10 +216,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_ids = set() for container_item in container_items_by_id.values(): repre_id = container_item.representation_id - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_info = repre_info_by_id.get(repre_id) if repre_info and repre_info.is_valid: filtered_items.append(container_item) @@ -754,10 +748,7 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): repre_id = container_item.representation_id - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_ids_by_project[project_name].add(repre_id) repre_info_by_id = {} for project_name, repre_ids in repre_ids_by_project.items(): @@ -773,10 +764,7 @@ class SceneInventoryView(QtWidgets.QTreeView): product_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): repre_id = container_item.representation_id - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_info = repre_info_by_id.get(repre_id) if not repre_info or not repre_info.is_valid: continue @@ -972,10 +960,6 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_ids_by_project = collections.defaultdict(set) for container_item in containers_items_by_id.values(): project_name = container_item.project_name - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) repre_id = container_item.representation_id repre_ids_by_project[project_name].add(repre_id) @@ -989,10 +973,7 @@ class SceneInventoryView(QtWidgets.QTreeView): product_ids_by_project = collections.defaultdict(set) for container_item in containers_items_by_id.values(): repre_id = container_item.representation_id - project_name = ( - container_item.project_name or - self._controller.get_current_project_name() - ) + project_name = container_item.project_name repre_info = repre_info_by_id.get(repre_id) if not repre_info or not repre_info.is_valid: continue From d2229fbb156bd9f74fc31b0a1c30d1b648f6d727 Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Mon, 18 Nov 2024 08:16:02 -0500 Subject: [PATCH 255/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_hierarchy.py | 11 +++++++---- .../plugins/publish/extract_hierarchy_to_ayon.py | 1 - 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 3340430345..531b6a1d76 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -13,7 +13,6 @@ class CollectHierarchy(pyblish.api.ContextPlugin): label = "Collect Hierarchy" order = pyblish.api.CollectorOrder - 0.076 - families = ["shot", "csv_ingest_shot"] hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, context): @@ -38,9 +37,12 @@ class CollectHierarchy(pyblish.api.ContextPlugin): ): continue - # exclude if not CSV ingest shot and not masterLayer True - if ("csv_ingest_shot" not in families and - not instance.data.get("heroTrack")): + # Skip if is not a hero track + # - skip check for traypubliser CSV ingest + if ( + not instance.data.get("heroTrack") + and "csv_ingest_shot" not in families + ): continue shot_data = { @@ -52,6 +54,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "comments": instance.data.get("comments", []), } + # TODO Fill in reason why we don't set attributes for csv_ingest_shot if "csv_ingest_shot" not in families: shot_data["attributes"] = { "handleStart": instance.data["handleStart"], diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index 390ce36126..25467fd94f 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -22,7 +22,6 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): order = pyblish.api.ExtractorOrder - 0.01 label = "Extract Hierarchy To AYON" - families = ["clip", "shot", "csv_ingest_shot"] def process(self, context): if not context.data.get("hierarchyContext"): From 004e9626ee5c9e3899bf72fee0af4c88e2b75b8a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 19 Nov 2024 00:19:48 +0800 Subject: [PATCH 256/546] big roy comment - refactoring the dict per repre_id per project --- client/ayon_core/tools/sceneinventory/model.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 5f9e6bb77c..7630e9ee45 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -135,7 +135,8 @@ class InventoryModel(QtGui.QStandardItemModel): self._clear_items() items_by_repre_id = collections.defaultdict(list) - repre_ids_by_project = collections.defaultdict(set) + item_by_repre_id_by_project_id = collections.defaultdict( + lambda: collections.defaultdict(set)) for container_item in container_items: # if ( # selected is not None @@ -145,20 +146,17 @@ class InventoryModel(QtGui.QStandardItemModel): project_name = container_item.project_name repre_id = container_item.representation_id items_by_repre_id[repre_id].append(container_item) - repre_ids_by_project[project_name].add(repre_id) + item_by_repre_id_by_project_id[project_name][repre_id].add(container_item) - repre_id = set(items_by_repre_id.keys()) repre_info_by_id = {} - for project_name, repre_ids in repre_ids_by_project.items(): + repre_id = set() + for project_name, repre_ids in item_by_repre_id_by_project_id.items(): + repre_ids = set(items_by_repre_id.keys()) repre_info = self._controller.get_representation_info_items( project_name, repre_ids ) repre_info_by_id.update(repre_info) - product_ids = { - repre_info.product_id - for repre_info in repre_info_by_id.values() - if repre_info.is_valid - } + repre_id.update(repre_ids) project_products = collections.defaultdict(set) for container_item in container_items: From 4a4377b489c643f6a4a60edebfcf7b9c59c4079b Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 18 Nov 2024 13:38:21 -0500 Subject: [PATCH 257/546] Rework to avoid csv_ingest_shot family. --- .../plugins/publish/collect_hierarchy.py | 51 ++++++++++--------- .../publish/collect_otio_frame_ranges.py | 4 ++ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 531b6a1d76..5e3be3d86d 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -31,18 +31,14 @@ class CollectHierarchy(pyblish.api.ContextPlugin): product_type = instance.data["productType"] families = instance.data["families"] - # exclude other families then self.families with intersection - if not set(self.families).intersection( - set(families + [product_type]) - ): + # exclude other families then "shot" with intersection + if "shot" not in (families + [product_type]): + self.log.debug("Skipping not a shot: {}".format(families)) continue # Skip if is not a hero track - # - skip check for traypubliser CSV ingest - if ( - not instance.data.get("heroTrack") - and "csv_ingest_shot" not in families - ): + if not instance.data.get("heroTrack"): + self.log.debug("Skipping not a shot from hero track") continue shot_data = { @@ -54,20 +50,29 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "comments": instance.data.get("comments", []), } - # TODO Fill in reason why we don't set attributes for csv_ingest_shot - if "csv_ingest_shot" not in families: - shot_data["attributes"] = { - "handleStart": instance.data["handleStart"], - "handleEnd": instance.data["handleEnd"], - "frameStart": instance.data["frameStart"], - "frameEnd": instance.data["frameEnd"], - "clipIn": instance.data["clipIn"], - "clipOut": instance.data["clipOut"], - "fps": instance.data["fps"], - "resolutionWidth": instance.data["resolutionWidth"], - "resolutionHeight": instance.data["resolutionHeight"], - "pixelAspect": instance.data["pixelAspect"], - } + shot_data["attributes"] = {} + SHOT_ATTRS = ( + "handleStart", + "handleEnd", + "frameStart", + "frameEnd", + "clipIn", + "clipOut", + "fps", + "resolutionWidth", + "resolutionHeight", + "pixelAspect", + ) + for shot_attr in SHOT_ATTRS: + if shot_attr not in instance.data: + # Shot attribute might not be defined (e.g. CSV ingest) + self.log.debug( + "%s shot attribute is not defined for instance.", + shot_attr + ) + continue + + shot_data["attributes"][shot_attr] = instance.data[shot_attr] # Split by '/' for AYON where asset is a path name = instance.data["folderPath"].split("/")[-1] diff --git a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py index d1c8d03212..62b4cefec6 100644 --- a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py +++ b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py @@ -29,6 +29,10 @@ class CollectOtioFrameRanges(pyblish.api.InstancePlugin): otio_range_with_handles ) + if not instance.data.get("otioClip"): + self.log.debug("Skipping collect OTIO frame range.") + return + # get basic variables otio_clip = instance.data["otioClip"] workfile_start = instance.data["workfileFrameStart"] From ff56393da87def3555cbebacb9d6907e27c2f523 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 18 Nov 2024 13:40:09 -0500 Subject: [PATCH 258/546] Fix linting. --- client/ayon_core/plugins/publish/collect_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 5e3be3d86d..4c606fdc10 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -61,7 +61,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "fps", "resolutionWidth", "resolutionHeight", - "pixelAspect", + "pixelAspect", ) for shot_attr in SHOT_ATTRS: if shot_attr not in instance.data: From f003d8af1d654460ab802a7b1a604f092a6d163d Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Mon, 18 Nov 2024 15:29:40 -0500 Subject: [PATCH 259/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/publish/lib.py | 28 +++++++++--------------- client/ayon_core/pipeline/stagingdir.py | 15 ++++++++----- client/ayon_core/pipeline/tempdir.py | 4 +--- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 67a65aec09..3fad15f1a2 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -687,33 +687,25 @@ def get_instance_staging_dir(instance): anatomy_data = instance.data["anatomyData"] template_data = copy.deepcopy(anatomy_data) - product_type = instance.data["productType"] - product_name = instance.data["productName"] - # context data based variables - project_entity = instance.context.data["projectEntity"] - folder_entity = instance.context.data["folderEntity"] - task_entity = instance.context.data["taskEntity"] - host_name = instance.context.data["hostName"] - project_settings = instance.context.data["project_settings"] - anatomy = instance.context.data["anatomy"] - current_file = instance.context.data.get("currentFile") + context = instance.context # add current file as workfile name into formatting data + current_file = context.data.get("currentFile") if current_file: workfile = os.path.basename(current_file) workfile_name, _ = os.path.splitext(workfile) template_data["workfile_name"] = workfile_name staging_dir_info = get_staging_dir_info( - host_name, - project_entity, - folder_entity, - task_entity, - product_type, - product_name, - anatomy, - project_settings=project_settings, + context.data["hostName"], + context.data["projectEntity"], + instance.data.get("folderEntity"), + instance.data.get("taskEntity"), + instance.data["productType"], + instance.data["productName"], + anatomy=context.data["anatomy"], + project_settings=context.data["project_settings"], template_data=template_data, ) diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index 818acef36a..1c658ac817 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -9,12 +9,12 @@ STAGING_DIR_TEMPLATES = "staging" def get_staging_dir_config( - host_name, project_name, task_type, task_name, product_type, product_name, + host_name, project_settings=None, anatomy=None, log=None, @@ -24,8 +24,8 @@ def get_staging_dir_config( Args: host_name (str): Name of host. project_name (str): Name of project. - task_type (str): Type of task. - task_name (str): Name of task. + task_type (Optional[str]): Type of task. + task_name (Optional[str]): Name of task. product_type (str): Type of product. product_name (str): Name of product. project_settings(Dict[str, Any]): Prepared project settings. @@ -103,13 +103,13 @@ def _validate_template_name(project_name, template_name, anatomy): def get_staging_dir_info( - host_name, project_entity, folder_entity, task_entity, product_type, product_name, - anatomy, + host_name, + anatomy=None, project_settings=None, template_data=None, always_return_path=None, @@ -157,6 +157,11 @@ def get_staging_dir_info( if always_return_path is None: always_return_path = True + if anatomy is None: + anatomy = Anatomy( + project_entity["name"], project_entity=project_entity + ) + if force_tmp_dir: return get_temp_dir( project_name=project_entity["name"], diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index 8a9334ecc2..b5f4a31ee7 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -86,9 +86,7 @@ def _create_custom_tempdir(project_name, anatomy=None): Returns: str | None: formatted path or None """ - env_tmpdir = os.getenv( - "AYON_TMPDIR", - ) + env_tmpdir = os.getenv("AYON_TMPDIR") if not env_tmpdir: return From e96133b34892bf0529f0bcc81e56822d5177ffbc Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 18 Nov 2024 16:34:31 -0500 Subject: [PATCH 260/546] Address feedback from PR. --- .../pipeline/create/creator_plugins.py | 2 +- client/ayon_core/pipeline/publish/lib.py | 1 + client/ayon_core/pipeline/stagingdir.py | 54 +++++++++---------- client/ayon_core/pipeline/tempdir.py | 11 ++-- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 4cbf432efd..667f70c27d 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -873,7 +873,7 @@ class Creator(BaseCreator): ) if not staging_dir_info: - return + return None staging_dir_path = staging_dir_info["stagingDir"] diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 3fad15f1a2..8d56deec04 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -707,6 +707,7 @@ def get_instance_staging_dir(instance): anatomy=context.data["anatomy"], project_settings=context.data["project_settings"], template_data=template_data, + always_return_path=True, ) staging_dir_path = staging_dir_info["stagingDir"] diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index 1c658ac817..07ef122337 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -77,7 +77,7 @@ def get_staging_dir_config( # template should always be found either from anatomy or from profile raise ValueError( "Staging dir profile is misconfigured! " - "No template was found for profile! " + f"No template was found for profile: {profile}! " "Check your project settings at: " "'ayon+settings://core/tools/publish/custom_staging_dir_profiles'" ) @@ -112,10 +112,11 @@ def get_staging_dir_info( anatomy=None, project_settings=None, template_data=None, - always_return_path=None, - force_tmp_dir=None, + always_return_path=True, + force_tmp_dir=False, logger=None, - **kwargs + prefix=None, + suffix=None, ): """Get staging dir info data. @@ -141,11 +142,8 @@ def get_staging_dir_info( force_tmp_dir (Optional[bool]): If True, staging dir will be created as tempdir. logger (Optional[logging.Logger]): Logger instance. - **kwargs: Arbitrary keyword arguments. See below. - - Keyword Arguments: - prefix (str): Prefix for staging dir. - suffix (str): Suffix for staging dir. + prefix (Optional[str]) Optional prefix for staging dir name. + suffix (Optional[str]): Optional suffix for staging dir name. Returns: Optional[Dict[str, Any]]: Staging dir info data @@ -153,10 +151,6 @@ def get_staging_dir_info( """ log = logger or Logger.get_logger("get_staging_dir_info") - # make sure always_return_path is set to true by default - if always_return_path is None: - always_return_path = True - if anatomy is None: anatomy = Anatomy( project_entity["name"], project_entity=project_entity @@ -166,11 +160,11 @@ def get_staging_dir_info( return get_temp_dir( project_name=project_entity["name"], anatomy=anatomy, - prefix=kwargs.get("prefix"), - suffix=kwargs.get("suffix"), + prefix=prefix, + suffix=suffix, ) - # making fewer queries to database + # making few queries to database ctx_data = get_template_data( project_entity, folder_entity, task_entity, host_name ) @@ -192,8 +186,8 @@ def get_staging_dir_info( staging_dir_config = get_staging_dir_config( host_name, project_entity["name"], - task_entity["type"], - task_entity["name"], + task_entity.get("type"), + task_entity.get("name"), product_type, product_name, project_settings=project_settings, @@ -201,19 +195,19 @@ def get_staging_dir_info( log=log, ) - # if no preset matching and always_get_some_dir is set, return tempdir - if not staging_dir_config and always_return_path: - return { - "stagingDir": get_temp_dir( - project_name=project_entity["name"], - anatomy=anatomy, - prefix=kwargs.get("prefix"), - suffix=kwargs.get("suffix"), - ), - "stagingDir_persistent": False, - } if not staging_dir_config: - return None + if always_return_path: # no config found but force an output + return { + "stagingDir": get_temp_dir( + project_name=project_entity["name"], + anatomy=anatomy, + prefix=kwargs.get("prefix"), + suffix=kwargs.get("suffix"), + ), + "stagingDir_persistent": False, + } + else: + return None return { "stagingDir": StringTemplate.format_template( diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index b5f4a31ee7..af2ff44a8f 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -5,6 +5,7 @@ Temporary folder operations import os import tempfile from pathlib import Path + from ayon_core.lib import StringTemplate from ayon_core.pipeline import Anatomy @@ -48,7 +49,7 @@ def get_temp_dir( # get customized tempdir path from `OPENPYPE_TMPDIR` env var custom_temp_dir = _create_custom_tempdir(anatomy.project_name, anatomy) - return _create_local_staging_dir(prefix, suffix, custom_temp_dir) + return _create_local_staging_dir(prefix, suffix, dirpath=custom_temp_dir) def _create_local_staging_dir(prefix, suffix, dirpath=None): @@ -70,7 +71,7 @@ def _create_local_staging_dir(prefix, suffix, dirpath=None): return staging_dir.as_posix() -def _create_custom_tempdir(project_name, anatomy=None): +def _create_custom_tempdir(project_name, anatomy): """ Create custom tempdir Template path formatting is supporting: @@ -81,19 +82,17 @@ def _create_custom_tempdir(project_name, anatomy=None): Args: project_name (str): project name - anatomy (ayon_core.pipeline.Anatomy)[optional]: Anatomy object + anatomy (ayon_core.pipeline.Anatomy): Anatomy object Returns: str | None: formatted path or None """ env_tmpdir = os.getenv("AYON_TMPDIR") if not env_tmpdir: - return + return None custom_tempdir = None if "{" in env_tmpdir: - if anatomy is None: - anatomy = Anatomy(project_name) # create base formate data template_data = { "root": anatomy.roots, From 7375538f22d84b8a4493fc84f77cfd4d12cd8fbc Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 18 Nov 2024 16:43:31 -0500 Subject: [PATCH 261/546] Fix lint. --- client/ayon_core/pipeline/stagingdir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index 07ef122337..2dd5c2f3eb 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -201,8 +201,8 @@ def get_staging_dir_info( "stagingDir": get_temp_dir( project_name=project_entity["name"], anatomy=anatomy, - prefix=kwargs.get("prefix"), - suffix=kwargs.get("suffix"), + prefix=prefix, + suffix=suffix, ), "stagingDir_persistent": False, } From 3078ba2a239c9780bf784d07849d42c2e02ea0c5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 19 Nov 2024 15:45:27 +0800 Subject: [PATCH 262/546] kuba's comment - add current project name as argument in the get_container_data and make sure the scene inventory won't show Entity N/A as name and version --- client/ayon_core/tools/sceneinventory/control.py | 2 +- client/ayon_core/tools/sceneinventory/model.py | 16 +++++++++------- .../tools/sceneinventory/models/containers.py | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 310e41b117..8c02881b82 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -4,7 +4,7 @@ from ayon_core.lib.events import QueuedEventSystem from ayon_core.host import HostBase from ayon_core.pipeline import ( registered_host, - get_current_context + get_current_context, ) from ayon_core.tools.common_models import HierarchyModel, ProjectsModel diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 7630e9ee45..5f9e6bb77c 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -135,8 +135,7 @@ class InventoryModel(QtGui.QStandardItemModel): self._clear_items() items_by_repre_id = collections.defaultdict(list) - item_by_repre_id_by_project_id = collections.defaultdict( - lambda: collections.defaultdict(set)) + repre_ids_by_project = collections.defaultdict(set) for container_item in container_items: # if ( # selected is not None @@ -146,17 +145,20 @@ class InventoryModel(QtGui.QStandardItemModel): project_name = container_item.project_name repre_id = container_item.representation_id items_by_repre_id[repre_id].append(container_item) - item_by_repre_id_by_project_id[project_name][repre_id].add(container_item) + repre_ids_by_project[project_name].add(repre_id) + repre_id = set(items_by_repre_id.keys()) repre_info_by_id = {} - repre_id = set() - for project_name, repre_ids in item_by_repre_id_by_project_id.items(): - repre_ids = set(items_by_repre_id.keys()) + for project_name, repre_ids in repre_ids_by_project.items(): repre_info = self._controller.get_representation_info_items( project_name, repre_ids ) repre_info_by_id.update(repre_info) - repre_id.update(repre_ids) + product_ids = { + repre_info.product_id + for repre_info in repre_info_by_id.values() + if repre_info.is_valid + } project_products = collections.defaultdict(set) for container_item in container_items: diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index c12a05fd99..e135cb0031 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -5,7 +5,6 @@ import ayon_api from ayon_api.graphql import GraphQlQuery from ayon_core.host import ILoadHost -from ayon_core.pipeline import get_current_project_name from ayon_core.tools.common_models.projects import StatusStates @@ -105,7 +104,7 @@ class ContainerItem: self.project_name = project_name @classmethod - def from_container_data(cls, container): + def from_container_data(cls, current_project_name, container): return cls( representation_id=container["representation"], loader_name=container["loader"], @@ -113,7 +112,7 @@ class ContainerItem: object_name=container["objectName"], item_id=uuid.uuid4().hex, project_name=container.get( - "project_name", get_current_project_name() + "project_name", current_project_name ) ) @@ -368,7 +367,8 @@ class ContainersModel: invalid_ids_mapping = {} for container in containers: try: - item = ContainerItem.from_container_data(container) + current_project_name = self._controller.get_current_project_name() + item = ContainerItem.from_container_data(current_project_name, container) repre_id = item.representation_id try: uuid.UUID(repre_id) From 83cc964ca041a95d6af426c39ec7b92ee7706bc5 Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Tue, 19 Nov 2024 08:07:52 -0500 Subject: [PATCH 263/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_hierarchy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 4c606fdc10..39501a9ed5 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -64,7 +64,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): "pixelAspect", ) for shot_attr in SHOT_ATTRS: - if shot_attr not in instance.data: + attr_value = instance.data.get(shot_attr) + if attr_value is None: # Shot attribute might not be defined (e.g. CSV ingest) self.log.debug( "%s shot attribute is not defined for instance.", @@ -72,7 +73,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): ) continue - shot_data["attributes"][shot_attr] = instance.data[shot_attr] + shot_data["attributes"][shot_attr] = attr_value # Split by '/' for AYON where asset is a path name = instance.data["folderPath"].split("/")[-1] From 70a38a6b1a3025ad653c2338dccfb30bdc9e0249 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 19 Nov 2024 08:22:13 -0500 Subject: [PATCH 264/546] Fix linting. --- client/ayon_core/plugins/publish/collect_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 39501a9ed5..cae89bd6bf 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -65,7 +65,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): ) for shot_attr in SHOT_ATTRS: attr_value = instance.data.get(shot_attr) - if attr_value is None: + if attr_value is None: # Shot attribute might not be defined (e.g. CSV ingest) self.log.debug( "%s shot attribute is not defined for instance.", From 26e5c2f52b05fd09c681305ffc151a3863aafbc5 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 19 Nov 2024 09:28:16 -0500 Subject: [PATCH 265/546] Adjust folder type creation in collect_hierarchy. --- client/ayon_core/plugins/publish/collect_hierarchy.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index cae89bd6bf..00f5c06c0b 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -43,9 +43,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): shot_data = { "entity_type": "folder", - # WARNING Default folder type is hardcoded - # suppose that all instances are Shots - "folder_type": "Shot", + # WARNING unless overwritten, default folder type is hardcoded to shot + "folder_type": instance.data.get("folder_type") or "Shot", "tasks": instance.data.get("tasks") or {}, "comments": instance.data.get("comments", []), } From b8ba7f47b0dee929fd1259e49fc791fa13ec3ed3 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 19 Nov 2024 16:49:08 -0500 Subject: [PATCH 266/546] Fix staging data computation. --- client/ayon_core/pipeline/create/creator_plugins.py | 8 +++++--- client/ayon_core/pipeline/publish/lib.py | 4 ++-- client/ayon_core/pipeline/stagingdir.py | 6 +++--- .../plugins/publish/collect_managed_staging_dir.py | 4 ++++ client/ayon_core/plugins/publish/extract_burnin.py | 2 +- .../ayon_core/plugins/publish/extract_color_transcode.py | 2 +- client/ayon_core/plugins/publish/extract_review.py | 2 +- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 667f70c27d..93e1f6f5cb 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -857,16 +857,18 @@ class Creator(BaseCreator): version = instance.get("version") if version is not None: template_data = {"version": version} + else: + template_data = {} staging_dir_info = get_staging_dir_info( - create_ctx.host_name, create_ctx.get_current_project_entity(), create_ctx.get_current_folder_entity(), create_ctx.get_current_task_entity(), product_type, product_name, - create_ctx.get_current_project_anatomy(), - create_ctx.get_current_project_settings(), + create_ctx.host_name, + anatomy=create_ctx.get_current_project_anatomy(), + project_settings=create_ctx.get_current_project_settings(), always_return_path=False, logger=self.log, template_data=template_data, diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 8d56deec04..4c36f473d1 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -653,12 +653,12 @@ def get_custom_staging_dir_info( DeprecationWarning, ) tr_data = get_staging_dir_config( - host_name, project_name, task_type, task_name, product_type, product_name, + host_name, project_settings=project_settings, anatomy=anatomy, log=log, @@ -698,12 +698,12 @@ def get_instance_staging_dir(instance): template_data["workfile_name"] = workfile_name staging_dir_info = get_staging_dir_info( - context.data["hostName"], context.data["projectEntity"], instance.data.get("folderEntity"), instance.data.get("taskEntity"), instance.data["productType"], instance.data["productName"], + context.data["hostName"], anatomy=context.data["anatomy"], project_settings=context.data["project_settings"], template_data=template_data, diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index 2dd5c2f3eb..c7cc95ff55 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -184,12 +184,12 @@ def get_staging_dir_info( # get staging dir config staging_dir_config = get_staging_dir_config( - host_name, project_entity["name"], - task_entity.get("type"), + task_entity.get("taskType"), task_entity.get("name"), product_type, product_name, + host_name, project_settings=project_settings, anatomy=anatomy, log=log, @@ -211,7 +211,7 @@ def get_staging_dir_info( return { "stagingDir": StringTemplate.format_template( - staging_dir_config["template"], ctx_data + staging_dir_config["template"]["directory"], ctx_data ), "stagingDir_persistent": staging_dir_config["persistence"], } diff --git a/client/ayon_core/plugins/publish/collect_managed_staging_dir.py b/client/ayon_core/plugins/publish/collect_managed_staging_dir.py index ca6d5161c1..1034b9a716 100644 --- a/client/ayon_core/plugins/publish/collect_managed_staging_dir.py +++ b/client/ayon_core/plugins/publish/collect_managed_staging_dir.py @@ -33,7 +33,11 @@ class CollectManagedStagingDir(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.4990 def process(self, instance): + """ Collect the staging data and stores it to the instance. + Args: + instance (object): The instance to inspect. + """ staging_dir_path = get_instance_staging_dir(instance) persistance = instance.data.get("stagingDir_persistent", False) diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 3eb4254a5e..8e8764fc33 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -254,7 +254,7 @@ class ExtractBurnin(publish.Extractor): if do_convert: new_staging_dir = get_temp_dir( project_name=instance.context.data["projectName"], - make_local=True, + use_local_temp=True, ) repre["stagingDir"] = new_staging_dir diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 4f0053c426..3c11a016ec 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -106,7 +106,7 @@ class ExtractOIIOTranscode(publish.Extractor): original_staging_dir = new_repre["stagingDir"] new_staging_dir = get_temp_dir( project_name=instance.context.data["projectName"], - make_local=True, + use_local_temp=True, ) new_repre["stagingDir"] = new_staging_dir diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 26cd2ef0b2..7c38b0453b 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -312,7 +312,7 @@ class ExtractReview(pyblish.api.InstancePlugin): if do_convert: new_staging_dir = get_temp_dir( project_name=instance.context.data["projectName"], - make_local=True, + use_local_temp=True, ) repre["stagingDir"] = new_staging_dir From d9c1a299b97ff8a4c5c6612a2644d41e7d564a40 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Nov 2024 17:23:36 +0800 Subject: [PATCH 267/546] loading the asset per repre_id and per project --- .../ayon_core/tools/sceneinventory/model.py | 246 +++++++++--------- .../tools/sceneinventory/models/containers.py | 22 +- 2 files changed, 127 insertions(+), 141 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 5f9e6bb77c..29818e387f 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -131,50 +131,39 @@ class InventoryModel(QtGui.QStandardItemModel): """Refresh the model""" # for debugging or testing, injecting items from outside container_items = self._controller.get_container_items() - self._clear_items() - - items_by_repre_id = collections.defaultdict(list) - repre_ids_by_project = collections.defaultdict(set) - for container_item in container_items: + repre_id = set() + repre_id_by_project_id = collections.defaultdict(set) + version_items_by_product_id = collections.defaultdict(dict) + repre_info_by_id_by_project = collections.defaultdict(list) + item_by_repre_id_by_project_id = collections.defaultdict( + lambda: collections.defaultdict(set)) + for project_name, container_item in container_items.items(): # if ( # selected is not None # and container_item.item_id not in selected # ): # continue - project_name = container_item.project_name - repre_id = container_item.representation_id - items_by_repre_id[repre_id].append(container_item) - repre_ids_by_project[project_name].add(repre_id) - - repre_id = set(items_by_repre_id.keys()) - repre_info_by_id = {} - for project_name, repre_ids in repre_ids_by_project.items(): + for item in container_item.values(): + representation_id = item.representation_id + if item.project_name != project_name: + continue + repre_id.add(representation_id) + item_by_repre_id_by_project_id[project_name][representation_id].add(item) repre_info = self._controller.get_representation_info_items( - project_name, repre_ids + project_name, repre_id ) - repre_info_by_id.update(repre_info) - product_ids = { - repre_info.product_id - for repre_info in repre_info_by_id.values() - if repre_info.is_valid - } + repre_info_by_id_by_project[project_name] = repre_info - project_products = collections.defaultdict(set) - for container_item in container_items: - representation_id = container_item.representation_id - project_name = container_item.project_name - repre_info = repre_info_by_id.get(representation_id) - if repre_info and repre_info.is_valid: - product_id = repre_info.product_id - project_products[project_name].add(product_id) - - version_items_by_product_id = {} - for project_name, product_ids in project_products.items(): + product_ids = { + repre_info.product_id + for repre_info in repre_info.values() + if repre_info.is_valid + } version_items = self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id.update(version_items) + version_items_by_product_id[project_name] = version_items # SiteSync addon information progress_by_id = self._controller.get_representations_site_progress( @@ -216,112 +205,113 @@ class InventoryModel(QtGui.QStandardItemModel): root_item = self.invisibleRootItem() group_items = [] - for repre_id, container_items in items_by_repre_id.items(): - repre_info = repre_info_by_id[repre_id] - version_color = None - if not repre_info.is_valid: - version_label = "N/A" - group_name = "< Entity N/A >" - item_icon = invalid_item_icon - is_latest = False - is_hero = False - status_name = None + for project_name, items_by_repre_id in item_by_repre_id_by_project_id.items(): + for repre_id, container_items in items_by_repre_id.items(): + repre_info = repre_info_by_id_by_project[project_name][repre_id] + version_color = None + if not repre_info.is_valid: + version_label = "N/A" + group_name = "< Entity N/A >" + item_icon = invalid_item_icon + is_latest = False + is_hero = False + status_name = None - else: - group_name = "{}_{}: ({})".format( - repre_info.folder_path.rsplit("/")[-1], - repre_info.product_name, - repre_info.representation_name + else: + group_name = "{}_{}: ({})".format( + repre_info.folder_path.rsplit("/")[-1], + repre_info.product_name, + repre_info.representation_name + ) + item_icon = valid_item_icon + + version_items = ( + version_items_by_product_id[project_name][repre_info.product_id] + ) + version_item = version_items[repre_info.version_id] + version_label = format_version(version_item.version) + is_hero = version_item.version < 0 + is_latest = version_item.is_latest + if not version_item.is_latest: + version_color = self.OUTDATED_COLOR + status_name = version_item.status + + status_color, status_short, status_icon = self._get_status_data( + status_name ) - item_icon = valid_item_icon - version_items = ( - version_items_by_product_id[repre_info.product_id] + repre_name = ( + repre_info.representation_name or "" ) - version_item = version_items[repre_info.version_id] - version_label = format_version(version_item.version) - is_hero = version_item.version < 0 - is_latest = version_item.is_latest - if not version_item.is_latest: - version_color = self.OUTDATED_COLOR - status_name = version_item.status + container_model_items = [] + for container_item in container_items: + object_name = container_item.object_name or "" + unique_name = repre_name + object_name + item = QtGui.QStandardItem() + item.setColumnCount(root_item.columnCount()) + item.setData(container_item.namespace, QtCore.Qt.DisplayRole) + item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE) + item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE) + item.setData(item_icon, QtCore.Qt.DecorationRole) + item.setData(repre_info.product_id, PRODUCT_ID_ROLE) + item.setData(container_item.item_id, ITEM_ID_ROLE) + item.setData(version_label, VERSION_LABEL_ROLE) + item.setData(container_item.loader_name, LOADER_NAME_ROLE) + item.setData(container_item.object_name, OBJECT_NAME_ROLE) + item.setData(container_item.project_name, PROJECT_NAME_ROLE) + item.setData(True, IS_CONTAINER_ITEM_ROLE) + item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) + container_model_items.append(item) + if not container_model_items: + continue - status_color, status_short, status_icon = self._get_status_data( - status_name - ) + progress = progress_by_id[repre_id] + active_site_progress = "{}%".format( + max(progress["active_site"], 0) * 100 + ) + remote_site_progress = "{}%".format( + max(progress["remote_site"], 0) * 100 + ) - repre_name = ( - repre_info.representation_name or "" - ) - container_model_items = [] - for container_item in container_items: - object_name = container_item.object_name or "" - unique_name = repre_name + object_name - item = QtGui.QStandardItem() - item.setColumnCount(root_item.columnCount()) - item.setData(container_item.namespace, QtCore.Qt.DisplayRole) - item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE) - item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE) - item.setData(item_icon, QtCore.Qt.DecorationRole) - item.setData(repre_info.product_id, PRODUCT_ID_ROLE) - item.setData(container_item.item_id, ITEM_ID_ROLE) - item.setData(version_label, VERSION_LABEL_ROLE) - item.setData(container_item.loader_name, LOADER_NAME_ROLE) - item.setData(container_item.object_name, OBJECT_NAME_ROLE) - item.setData(container_item.project_name, PROJECT_NAME_ROLE) - item.setData(True, IS_CONTAINER_ITEM_ROLE) - item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) - container_model_items.append(item) - if not container_model_items: - continue + group_item = QtGui.QStandardItem() + group_item.setColumnCount(root_item.columnCount()) + group_item.setData(group_name, QtCore.Qt.DisplayRole) + group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE) + group_item.setData(group_item_icon, QtCore.Qt.DecorationRole) + group_item.setData(group_item_font, QtCore.Qt.FontRole) + group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE) + group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE) + group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE) + group_item.setData(is_latest, VERSION_IS_LATEST_ROLE) + group_item.setData(is_hero, VERSION_IS_HERO_ROLE) + group_item.setData(version_label, VERSION_LABEL_ROLE) + group_item.setData(len(container_items), COUNT_ROLE) + group_item.setData(status_name, STATUS_NAME_ROLE) + group_item.setData(status_short, STATUS_SHORT_ROLE) + group_item.setData(status_color, STATUS_COLOR_ROLE) + group_item.setData(status_icon, STATUS_ICON_ROLE) - progress = progress_by_id[repre_id] - active_site_progress = "{}%".format( - max(progress["active_site"], 0) * 100 - ) - remote_site_progress = "{}%".format( - max(progress["remote_site"], 0) * 100 - ) - - group_item = QtGui.QStandardItem() - group_item.setColumnCount(root_item.columnCount()) - group_item.setData(group_name, QtCore.Qt.DisplayRole) - group_item.setData(group_name, ITEM_UNIQUE_NAME_ROLE) - group_item.setData(group_item_icon, QtCore.Qt.DecorationRole) - group_item.setData(group_item_font, QtCore.Qt.FontRole) - group_item.setData(repre_info.product_id, PRODUCT_ID_ROLE) - group_item.setData(repre_info.product_type, PRODUCT_TYPE_ROLE) - group_item.setData(product_type_icon, PRODUCT_TYPE_ICON_ROLE) - group_item.setData(is_latest, VERSION_IS_LATEST_ROLE) - group_item.setData(is_hero, VERSION_IS_HERO_ROLE) - group_item.setData(version_label, VERSION_LABEL_ROLE) - group_item.setData(len(container_items), COUNT_ROLE) - group_item.setData(status_name, STATUS_NAME_ROLE) - group_item.setData(status_short, STATUS_SHORT_ROLE) - group_item.setData(status_color, STATUS_COLOR_ROLE) - group_item.setData(status_icon, STATUS_ICON_ROLE) - - group_item.setData( - active_site_progress, ACTIVE_SITE_PROGRESS_ROLE - ) - group_item.setData( - remote_site_progress, REMOTE_SITE_PROGRESS_ROLE - ) - group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE) - group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE) - group_item.setData(False, IS_CONTAINER_ITEM_ROLE) - - if version_color is not None: - group_item.setData(version_color, VERSION_COLOR_ROLE) - - if repre_info.product_group: group_item.setData( - repre_info.product_group, PRODUCT_GROUP_NAME_ROLE + active_site_progress, ACTIVE_SITE_PROGRESS_ROLE ) - group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE) + group_item.setData( + remote_site_progress, REMOTE_SITE_PROGRESS_ROLE + ) + group_item.setData(active_site_icon, ACTIVE_SITE_ICON_ROLE) + group_item.setData(remote_site_icon, REMOTE_SITE_ICON_ROLE) + group_item.setData(False, IS_CONTAINER_ITEM_ROLE) - group_item.appendRows(container_model_items) - group_items.append(group_item) + if version_color is not None: + group_item.setData(version_color, VERSION_COLOR_ROLE) + + if repre_info.product_group: + group_item.setData( + repre_info.product_group, PRODUCT_GROUP_NAME_ROLE + ) + group_item.setData(group_icon, PRODUCT_GROUP_ICON_ROLE) + + group_item.appendRows(container_model_items) + group_items.append(group_item) if group_items: root_item.appendRows(group_items) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index e135cb0031..ca67fb59f9 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -191,7 +191,7 @@ class VersionItem: class ContainersModel: def __init__(self, controller): self._controller = controller - self._items_cache = None + self._project_cache = None self._containers_by_id = {} self._container_items_by_id = {} self._container_items_by_project = {} @@ -200,7 +200,7 @@ class ContainersModel: self._product_ids_by_project = {} def reset(self): - self._items_cache = None + self._project_cache = None self._containers_by_id = {} self._container_items_by_id = {} self._container_items_by_project = {} @@ -220,7 +220,7 @@ class ContainersModel: def get_container_items(self): self._update_cache() - return list(self._items_cache) + return self._project_cache def get_container_items_by_id(self, item_ids): return { @@ -248,7 +248,6 @@ class ContainersModel: repre_hierarchy_by_id = get_representations_hierarchy( project_name, missing_repre_ids ) - self._product_ids_by_project[project_name] = set() for repre_id, repre_hierarchy in repre_hierarchy_by_id.items(): kwargs = { "folder_id": None, @@ -280,8 +279,6 @@ class ContainersModel: repre_info = RepresentationInfo(**kwargs) self._repre_info_by_id[repre_id] = repre_info - self._product_ids_by_project[project_name].add( - repre_info.product_id) output[repre_id] = repre_info return output @@ -293,7 +290,6 @@ class ContainersModel: for product_id in product_ids if product_id not in self._version_items_by_product_id } - current_product_ids = self._product_ids_by_project.get(project_name) if missing_ids: status_items_by_name = { status_item.name: status_item @@ -302,14 +298,13 @@ class ContainersModel: def version_sorted(entity): return entity["version"] - current_missing_ids = current_product_ids.intersection(missing_ids) version_entities_by_product_id = { product_id: [] - for product_id in current_missing_ids + for product_id in missing_ids } version_entities = list(ayon_api.get_versions( project_name, - product_ids=current_missing_ids, + product_ids=missing_ids, fields={"id", "version", "productId", "status"} )) version_entities.sort(key=version_sorted) @@ -349,7 +344,7 @@ class ContainersModel: } def _update_cache(self): - if self._items_cache is not None: + if self._project_cache is not None: return host = self._controller.get_host() @@ -363,7 +358,7 @@ class ContainersModel: container_items = [] containers_by_id = {} container_items_by_id = {} - project_name_by_repre_id = {} + project_cache = collections.defaultdict(dict) invalid_ids_mapping = {} for container in containers: try: @@ -388,9 +383,10 @@ class ContainersModel: containers_by_id[item.item_id] = container container_items_by_id[item.item_id] = item - project_name_by_repre_id[item.representation_id] = item.project_name + project_cache[item.project_name] = container_items_by_id container_items.append(item) self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id self._items_cache = container_items + self._project_cache = project_cache From 7a224914ea7dc59bcfa158cd6ead2404c4074a9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Nov 2024 17:43:17 +0800 Subject: [PATCH 268/546] remove unused variable --- client/ayon_core/tools/sceneinventory/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 29818e387f..640a8017ab 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -133,7 +133,6 @@ class InventoryModel(QtGui.QStandardItemModel): container_items = self._controller.get_container_items() self._clear_items() repre_id = set() - repre_id_by_project_id = collections.defaultdict(set) version_items_by_product_id = collections.defaultdict(dict) repre_info_by_id_by_project = collections.defaultdict(list) item_by_repre_id_by_project_id = collections.defaultdict( From 582dce426fb2005fe8878e69a5f191a87ba4e073 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:35:24 +0100 Subject: [PATCH 269/546] Fix typo --- client/ayon_core/tools/creator/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/creator/widgets.py b/client/ayon_core/tools/creator/widgets.py index 53a2ee1080..09f4e1fa32 100644 --- a/client/ayon_core/tools/creator/widgets.py +++ b/client/ayon_core/tools/creator/widgets.py @@ -104,7 +104,7 @@ class ProductNameValidator(RegularExpressionValidatorClass): def validate(self, text, pos): results = super(ProductNameValidator, self).validate(text, pos) - if results[0] == self.Invalid: + if results[0] == self.invalid: self.invalid.emit(self.invalid_chars(text)) return results From 5ccdfc258a97e519e97cac5ac0d254678f27f1aa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:36:21 +0100 Subject: [PATCH 270/546] Fix plugins returning empty list --- client/ayon_core/tools/pyblish_pype/model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/pyblish_pype/model.py b/client/ayon_core/tools/pyblish_pype/model.py index 3a402f386e..7c242c817a 100644 --- a/client/ayon_core/tools/pyblish_pype/model.py +++ b/client/ayon_core/tools/pyblish_pype/model.py @@ -780,6 +780,8 @@ class InstanceModel(QtGui.QStandardItemModel): def update_with_result(self, result): instance = result["instance"] + if isinstance(instance, list): + instance = instance.pop() if instance else None if instance is None: instance_id = self.controller.context.id else: @@ -976,6 +978,8 @@ class TerminalModel(QtGui.QStandardItemModel): prepared_records = [] instance_name = None instance = result["instance"] + if isinstance(instance, list): + instance = instance.pop() if instance else None if instance is not None: instance_name = instance.data["name"] From c56cd07e67b22b4e34cf6c246229150799fa5ab0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:47:37 +0100 Subject: [PATCH 271/546] Provided backward compatibility for prepare_representations --- client/ayon_core/pipeline/farm/pyblish_functions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index e9f179c668..e236ec6c3d 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -333,7 +333,13 @@ def prepare_representations( log = Logger.get_logger("farm_publishing") - frames_to_render = _get_real_frames_to_render(frames_to_render) + if frames_to_render is not None: + frames_to_render = _get_real_frames_to_render(frames_to_render) + else: + # Backwards compatibility for older logic + frame_start = int(skeleton_data.get("frameStartHandle")) + frame_end = int(skeleton_data.get("frameEndHandle")) + frames_to_render = list(range(frame_start, frame_end + 1)) # create representation for every collected sequence for collection in collections: From c2716872d43d20f1e7de051375244893cd192d38 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:49:24 +0100 Subject: [PATCH 272/546] Do not convert to str unnecessary Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index e9f179c668..aa69633a22 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -482,7 +482,8 @@ def _get_real_frames_to_render(frames): range(int(splitted[0]), int(splitted[1])+1)) else: frames_to_render.append(frame) - return [str(frame_to_render) for frame_to_render in frames_to_render] + frames_to_render.sort() + return frames_to_render def _get_real_files_to_rendered(collection, frames_to_render): From de88260ddac22419b3a87606aaae08bfc9ec4e09 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:50:23 +0100 Subject: [PATCH 273/546] frames_to_render are now list of integers Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index aa69633a22..876a5b504f 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -481,7 +481,7 @@ def _get_real_frames_to_render(frames): frames_to_render.extend( range(int(splitted[0]), int(splitted[1])+1)) else: - frames_to_render.append(frame) + frames_to_render.append(int(frame)) frames_to_render.sort() return frames_to_render From 630d2d49130a9cea142aea999203fc00106a269d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:50:36 +0100 Subject: [PATCH 274/546] frames_to_render are now list of integers Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 876a5b504f..6740950d78 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -371,8 +371,8 @@ def prepare_representations( " This may cause issues on farm." ).format(staging)) - frame_start = int(frames_to_render[0]) - frame_end = int(frames_to_render[-1]) + frame_start = frames_to_render[0] + frame_end = frames_to_render[-1] if skeleton_data.get("slate"): frame_start -= 1 From 5f3175258725853011ab4b9eb2c58c1fc6959eda Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:50:59 +0100 Subject: [PATCH 275/546] Used comprehension Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 6740950d78..09df371371 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -498,11 +498,10 @@ def _get_real_files_to_rendered(collection, frames_to_render): file_name, extracted_frame = list(collect_frames(files).items())[0] if extracted_frame: found_frame_pattern_length = len(extracted_frame) - normalized_frames_to_render = set() - for frame_to_render in frames_to_render: - normalized_frames_to_render.add( - str(frame_to_render).zfill(found_frame_pattern_length) - ) + normalized_frames_to_render = { + str(frame_to_render).zfill(found_frame_pattern_length) + for frame_to_render in frames_to_render + } filtered_files = [] for file_name in files: From fd20885ac26366b996c2b14d102cbefdc3a75341 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:51:12 +0100 Subject: [PATCH 276/546] Used comprehension Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/pipeline/farm/pyblish_functions.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 09df371371..4ba088f92c 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -503,13 +503,14 @@ def _get_real_files_to_rendered(collection, frames_to_render): for frame_to_render in frames_to_render } - filtered_files = [] - for file_name in files: - if any(frame in file_name - for frame in normalized_frames_to_render): - filtered_files.append(file_name) - - files = filtered_files + files = [ + filename + for filename in files + if any( + frame in filename + for frame in normalized_frames_to_render + ) + ] return files From 80ab628d44b083f168e1a7e822f4f1145d40145b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 12:52:51 +0100 Subject: [PATCH 277/546] Changed condition to bail early --- .../pipeline/farm/pyblish_functions.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index e236ec6c3d..37a018e116 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -501,21 +501,23 @@ def _get_real_files_to_rendered(collection, frames_to_render): """ files = [os.path.basename(f) for f in list(collection)] file_name, extracted_frame = list(collect_frames(files).items())[0] - if extracted_frame: - found_frame_pattern_length = len(extracted_frame) - normalized_frames_to_render = set() - for frame_to_render in frames_to_render: - normalized_frames_to_render.add( - str(frame_to_render).zfill(found_frame_pattern_length) - ) + if not extracted_frame: + return files - filtered_files = [] - for file_name in files: - if any(frame in file_name - for frame in normalized_frames_to_render): - filtered_files.append(file_name) + found_frame_pattern_length = len(extracted_frame) + normalized_frames_to_render = set() + for frame_to_render in frames_to_render: + normalized_frames_to_render.add( + str(frame_to_render).zfill(found_frame_pattern_length) + ) - files = filtered_files + filtered_files = [] + for file_name in files: + if any(frame in file_name + for frame in normalized_frames_to_render): + filtered_files.append(file_name) + + files = filtered_files return files From 0c5777910a7e96acde12cdf11b7df86d67e6a5e2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 14:26:07 +0100 Subject: [PATCH 278/546] Revert "Fix plugins returning empty list" This reverts commit 5ccdfc258a97e519e97cac5ac0d254678f27f1aa. --- client/ayon_core/tools/pyblish_pype/model.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/tools/pyblish_pype/model.py b/client/ayon_core/tools/pyblish_pype/model.py index 7c242c817a..3a402f386e 100644 --- a/client/ayon_core/tools/pyblish_pype/model.py +++ b/client/ayon_core/tools/pyblish_pype/model.py @@ -780,8 +780,6 @@ class InstanceModel(QtGui.QStandardItemModel): def update_with_result(self, result): instance = result["instance"] - if isinstance(instance, list): - instance = instance.pop() if instance else None if instance is None: instance_id = self.controller.context.id else: @@ -978,8 +976,6 @@ class TerminalModel(QtGui.QStandardItemModel): prepared_records = [] instance_name = None instance = result["instance"] - if isinstance(instance, list): - instance = instance.pop() if instance else None if instance is not None: instance_name = instance.data["name"] From 962df74e640f9661489b2ac6433a55d913fcd07d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 14:27:06 +0100 Subject: [PATCH 279/546] Better RegularExpressionValidatorClass.Invalid used --- client/ayon_core/tools/creator/widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/creator/widgets.py b/client/ayon_core/tools/creator/widgets.py index 09f4e1fa32..96ce899881 100644 --- a/client/ayon_core/tools/creator/widgets.py +++ b/client/ayon_core/tools/creator/widgets.py @@ -104,7 +104,7 @@ class ProductNameValidator(RegularExpressionValidatorClass): def validate(self, text, pos): results = super(ProductNameValidator, self).validate(text, pos) - if results[0] == self.invalid: + if results[0] == RegularExpressionValidatorClass.Invalid: self.invalid.emit(self.invalid_chars(text)) return results From 63592f9e2bb799350338e5f29e39cdc59bd28077 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 14:32:54 +0100 Subject: [PATCH 280/546] Used comprehension Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index e1d83a175e..e3470f4c41 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -507,11 +507,10 @@ def _get_real_files_to_rendered(collection, frames_to_render): return files found_frame_pattern_length = len(extracted_frame) - normalized_frames_to_render = set() - for frame_to_render in frames_to_render: - normalized_frames_to_render.add( - str(frame_to_render).zfill(found_frame_pattern_length) - ) + normalized_frames_to_render = { + str(frame_to_render).zfill(found_frame_pattern_length) + for frame_to_render in frames_to_render + } filtered_files = [] for file_name in files: From 112c4bdc0eec32f7d140964a911d88c5926a27e7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 21 Nov 2024 14:33:08 +0100 Subject: [PATCH 281/546] Used comprehension Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/pipeline/farm/pyblish_functions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index e3470f4c41..16364a17ee 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -512,14 +512,14 @@ def _get_real_files_to_rendered(collection, frames_to_render): for frame_to_render in frames_to_render } - filtered_files = [] - for file_name in files: - if any(frame in file_name - for frame in normalized_frames_to_render): - filtered_files.append(file_name) - - files = filtered_files - return files + return [ + file_name + for file_name in files + if any( + frame in file_name + for frame in normalized_frames_to_render + ) + ] def create_instances_for_aov(instance, skeleton, aov_filter, From c1b83046b7d7bfc3d0284f1134d4966bebda872a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 21 Nov 2024 13:33:12 +0000 Subject: [PATCH 282/546] [Automated] Add generated package files to main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 63f7de04dc..e75a940be9 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.8+dev" +__version__ = "1.0.9" diff --git a/package.py b/package.py index bbfcc51019..c4da1ded1e 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.8+dev" +version = "1.0.9" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e29aa08c6d..1a7882bdb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.8+dev" +version = "1.0.9" description = "" authors = ["Ynput Team "] readme = "README.md" From 8bfcd92f1c7383bd069c7e259f3ebcdbeb8cab41 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 21 Nov 2024 13:33:50 +0000 Subject: [PATCH 283/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index e75a940be9..ab8c9424fa 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.9" +__version__ = "1.0.9+dev" diff --git a/package.py b/package.py index c4da1ded1e..b90db4cde4 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.9" +version = "1.0.9+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 1a7882bdb5..f2d09d925d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.9" +version = "1.0.9+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From ddf0a2b00a6cad3677924ec25ef6341daeb92001 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:00:43 +0100 Subject: [PATCH 284/546] force to use older opencolorio than 2.4.0 --- client/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyproject.toml b/client/pyproject.toml index a0be9605b6..20b51ff219 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -15,6 +15,6 @@ qtawesome = "0.7.3" aiohttp-middlewares = "^2.0.0" Click = "^8" OpenTimelineIO = "0.16.0" -opencolorio = "^2.3.2" +opencolorio = "<2.4.0" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" From 5b35547072b89f25c4c53d41612fa7d5b27b1d6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 21 Nov 2024 15:10:57 +0100 Subject: [PATCH 285/546] fix syntax of version requirement --- client/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/pyproject.toml b/client/pyproject.toml index 20b51ff219..edf7f57317 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -15,6 +15,6 @@ qtawesome = "0.7.3" aiohttp-middlewares = "^2.0.0" Click = "^8" OpenTimelineIO = "0.16.0" -opencolorio = "<2.4.0" +opencolorio = "^2.3.2,<2.4.0" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" From a70135bb5156fc3883047561edcda702f81f8731 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Nov 2024 22:35:25 +0800 Subject: [PATCH 286/546] implement switch and set version by repre_id/product_id per project --- .../ayon_core/tools/sceneinventory/model.py | 20 +- .../tools/sceneinventory/models/containers.py | 11 +- client/ayon_core/tools/sceneinventory/view.py | 190 +++++++++--------- 3 files changed, 113 insertions(+), 108 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 640a8017ab..75af957cfa 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -133,24 +133,26 @@ class InventoryModel(QtGui.QStandardItemModel): container_items = self._controller.get_container_items() self._clear_items() repre_id = set() + repre_ids_by_project = collections.defaultdict(set) version_items_by_product_id = collections.defaultdict(dict) - repre_info_by_id_by_project = collections.defaultdict(list) + repre_info_by_id_by_project = collections.defaultdict(dict) item_by_repre_id_by_project_id = collections.defaultdict( lambda: collections.defaultdict(set)) - for project_name, container_item in container_items.items(): + for container_item in container_items: # if ( # selected is not None # and container_item.item_id not in selected # ): # continue - for item in container_item.values(): - representation_id = item.representation_id - if item.project_name != project_name: - continue - repre_id.add(representation_id) - item_by_repre_id_by_project_id[project_name][representation_id].add(item) + project_name = container_item.project_name + representation_id = container_item.representation_id + repre_id.add(representation_id) + repre_ids_by_project[project_name].add(representation_id) + item_by_repre_id_by_project_id[project_name][representation_id].add(container_item) + + for project_name, representation_ids in repre_ids_by_project.items(): repre_info = self._controller.get_representation_info_items( - project_name, repre_id + project_name, representation_ids ) repre_info_by_id_by_project[project_name] = repre_info diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index ca67fb59f9..dc41bdc8fa 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -191,7 +191,7 @@ class VersionItem: class ContainersModel: def __init__(self, controller): self._controller = controller - self._project_cache = None + self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} self._container_items_by_project = {} @@ -200,7 +200,7 @@ class ContainersModel: self._product_ids_by_project = {} def reset(self): - self._project_cache = None + self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} self._container_items_by_project = {} @@ -220,7 +220,7 @@ class ContainersModel: def get_container_items(self): self._update_cache() - return self._project_cache + return list(self._items_cache) def get_container_items_by_id(self, item_ids): return { @@ -344,7 +344,7 @@ class ContainersModel: } def _update_cache(self): - if self._project_cache is not None: + if self._items_cache is not None: return host = self._controller.get_host() @@ -358,7 +358,6 @@ class ContainersModel: container_items = [] containers_by_id = {} container_items_by_id = {} - project_cache = collections.defaultdict(dict) invalid_ids_mapping = {} for container in containers: try: @@ -383,10 +382,8 @@ class ContainersModel: containers_by_id[item.item_id] = container container_items_by_id[item.item_id] = item - project_cache[item.project_name] = container_items_by_id container_items.append(item) self._containers_by_id = containers_by_id self._container_items_by_id = container_items_by_id self._items_cache = container_items - self._project_cache = project_cache diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index a049fd1e0b..24e0195e31 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -197,26 +197,29 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_id = container_item.representation_id project_name = container_item.project_name repre_ids_by_project[project_name].add(repre_id) - repre_info_by_id = {} + + repre_info_by_project = collections.defaultdict(dict) for project_name, repre_ids in repre_ids_by_project.items(): repre_info = self._controller.get_representation_info_items( project_name, repre_ids) - repre_info_by_id.update(repre_info) - - valid_repre_ids = { - repre_id - for repre_id, repre_info in repre_info_by_id.items() - if repre_info.is_valid - } - + repre_info_by_project[project_name].update(repre_info) # Exclude items that are "NOT FOUND" since setting versions, updating # and removal won't work for those items. filtered_items = [] - product_ids_by_project = collections.defaultdict(set) version_ids = set() + valid_repre_ids = set() + product_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): - repre_id = container_item.representation_id project_name = container_item.project_name + repre_info_by_id = repre_info_by_project.get(project_name) + repre_id = container_item.representation_id + all_valid_repre_ids = { + repre_id + for repre_id, repre_info in repre_info_by_id.items() + if repre_info.is_valid + } + valid_repre_ids.update(all_valid_repre_ids) + repre_info = repre_info_by_id.get(repre_id) if repre_info and repre_info.is_valid: filtered_items.append(container_item) @@ -233,47 +236,47 @@ class SceneInventoryView(QtWidgets.QTreeView): # Keep remove action for invalid items menu.addAction(remove_action) return - version_items_by_product_id = {} + version_items_by_product_id_by_project = collections.defaultdict(dict) for project_name, product_ids in product_ids_by_project.items(): version_items = self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id.update(version_items -) + version_items_by_product_id_by_project[project_name] = version_items has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False has_outdated_approved = False last_version_by_product_id = {} - for product_id, version_items_by_id in ( - version_items_by_product_id.items() - ): - _has_outdated_approved = False - _last_approved_version_item = None - for version_item in version_items_by_id.values(): - if version_item.is_hero: - has_available_hero_version = True - - elif version_item.is_last_approved: - _last_approved_version_item = version_item - _has_outdated_approved = True - - if version_item.version_id not in version_ids: - continue - - if version_item.is_hero: - has_loaded_hero_versions = True - elif not version_item.is_latest: - has_outdated = True - - if ( - _has_outdated_approved - and _last_approved_version_item is not None + for version_items_by_product_id in version_items_by_product_id_by_project.values(): + for product_id, version_items_by_id in ( + version_items_by_product_id.items() ): - last_version_by_product_id[product_id] = ( - _last_approved_version_item - ) - has_outdated_approved = True + _has_outdated_approved = False + _last_approved_version_item = None + for version_item in version_items_by_id.values(): + if version_item.is_hero: + has_available_hero_version = True + + elif version_item.is_last_approved: + _last_approved_version_item = version_item + _has_outdated_approved = True + + if version_item.version_id not in version_ids: + continue + + if version_item.is_hero: + has_loaded_hero_versions = True + elif not version_item.is_latest: + has_outdated = True + + if ( + _has_outdated_approved + and _last_approved_version_item is not None + ): + last_version_by_product_id[product_id] = ( + _last_approved_version_item + ) + has_outdated_approved = True switch_to_versioned = None if has_loaded_hero_versions: @@ -294,8 +297,9 @@ class SceneInventoryView(QtWidgets.QTreeView): approved_version_by_item_id = {} if has_outdated_approved: for container_item in container_items_by_id.values(): + project_name = container_item.project_name repre_id = container_item.representation_id - repre_info = repre_info_by_id.get(repre_id) + repre_info = repre_info_by_project[project_name][repre_id] if not repre_info or not repre_info.is_valid: continue version_item = last_version_by_product_id.get( @@ -750,54 +754,53 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_id = container_item.representation_id project_name = container_item.project_name repre_ids_by_project[project_name].add(repre_id) - repre_info_by_id = {} + + versions = set() + repre_info_by_project = collections.defaultdict(dict) + version_items_by_product_id_by_project = collections.defaultdict(dict) for project_name, repre_ids in repre_ids_by_project.items(): repre_info = self._controller.get_representation_info_items( project_name, repre_ids ) - repre_info_by_id.update(repre_info) + repre_info_by_project[project_name].update(repre_info) - product_ids = { - repre_info.product_id - for repre_info in repre_info_by_id.values() - } - product_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): - repre_id = container_item.representation_id project_name = container_item.project_name + repre_info_by_id = repre_info_by_project.get(project_name) + repre_id = container_item.representation_id repre_info = repre_info_by_id.get(repre_id) - if not repre_info or not repre_info.is_valid: - continue - product_ids_by_project[project_name].add( + product_ids = { repre_info.product_id - ) - active_repre_info = repre_info_by_id[active_repre_id] - active_version_id = active_repre_info.version_id - active_product_id = active_repre_info.product_id - version_items_by_product_id = {} - for project_name, project_product_ids in product_ids_by_project.items(): + for repre_info in repre_info_by_id.values() + if repre_info.is_valid + } version_items = self._controller.get_version_items( - project_name, project_product_ids + project_name, product_ids ) - version_items_by_product_id.update(version_items) - version_items = list( - version_items_by_product_id[active_product_id].values() - ) - versions = {version_item.version for version_item in version_items} + version_items_by_product_id_by_project[project_name] = version_items + active_repre_info = repre_info_by_id[active_repre_id] + active_version_id = active_repre_info.version_id + active_product_id = active_repre_info.product_id + version_items = list( + version_items_by_product_id_by_project[project_name][active_product_id].values() + ) + all_versions = {version_item.version for version_item in version_items} + versions.update(all_versions) product_ids_by_version = collections.defaultdict(set) - for version_items_by_id in version_items_by_product_id.values(): - for version_item in version_items_by_id.values(): - version = version_item.version - _prod_version = version - if _prod_version < 0: - _prod_version = -1 - product_ids_by_version[_prod_version].add( - version_item.product_id - ) - if version in versions: - continue - versions.add(version) - version_items.append(version_item) + for version_items_by_product_id in version_items_by_product_id_by_project.values(): + for version_items_by_id in version_items_by_product_id.values(): + for version_item in version_items_by_id.values(): + version = version_item.version + _prod_version = version + if _prod_version < 0: + _prod_version = -1 + product_ids_by_version[_prod_version].add( + version_item.product_id + ) + if version in versions: + continue + versions.add(version) + version_items.append(version_item) def version_sorter(item): hero_value = 0 @@ -862,8 +865,9 @@ class SceneInventoryView(QtWidgets.QTreeView): filtered_item_ids = set() for container_item in container_items_by_id.values(): + project_name = container_item.project_name repre_id = container_item.representation_id - repre_info = repre_info_by_id[repre_id] + repre_info = repre_info_by_project[project_name][repre_id] if repre_info.product_id in product_ids: filtered_item_ids.add(container_item.item_id) @@ -964,37 +968,39 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_ids_by_project[project_name].add(repre_id) # Get representation info items by ID - repre_info_by_id = {} + repre_info_by_project = collections.defaultdict(dict) for project_name, repre_ids in repre_ids_by_project.items(): repre_info = self._controller.get_representation_info_items( project_name, repre_ids) - repre_info_by_id.update(repre_info) + repre_info_by_project[project_name].update(repre_info) product_ids_by_project = collections.defaultdict(set) + version_items_by_product_id_by_project = collections.defaultdict(dict) for container_item in containers_items_by_id.values(): - repre_id = container_item.representation_id project_name = container_item.project_name + repre_info_by_id = repre_info_by_project.get(project_name) + repre_id = container_item.representation_id repre_info = repre_info_by_id.get(repre_id) - if not repre_info or not repre_info.is_valid: - continue - product_ids_by_project[project_name].add( + product_ids = { repre_info.product_id - ) - - version_items_by_product_id = {} - for project_name, product_ids in product_ids_by_project.items(): + for repre_info in repre_info_by_id.values() + if repre_info.is_valid + } version_items = self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id.update(version_items) + version_items_by_product_id_by_project[project_name] = version_items update_containers = [] update_versions = [] for item_id, container_item in containers_items_by_id.items(): repre_id = container_item.representation_id + project_name = container_item.project_name repre_info = repre_info_by_id[repre_id] product_id = repre_info.product_id - version_items_id = version_items_by_product_id[product_id] + version_items_id = ( + version_items_by_product_id_by_project[project_name][product_id] + ) version_item = version_items_id.get(repre_info.version_id, {}) if not version_item or not version_item.is_hero: continue From 77e5317ee3bae6b2aa792a9525cc3433e24ddeca Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Nov 2024 22:37:55 +0800 Subject: [PATCH 287/546] remove unused variable --- client/ayon_core/tools/sceneinventory/view.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 24e0195e31..025bff6e9f 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -974,7 +974,6 @@ class SceneInventoryView(QtWidgets.QTreeView): project_name, repre_ids) repre_info_by_project[project_name].update(repre_info) - product_ids_by_project = collections.defaultdict(set) version_items_by_product_id_by_project = collections.defaultdict(dict) for container_item in containers_items_by_id.values(): project_name = container_item.project_name From 26251bb9b4ebae496ab7aab0a7a3e0a1a95c1611 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:29:31 +0100 Subject: [PATCH 288/546] simplified parsing of template --- client/ayon_core/lib/path_templates.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index dc88ec956b..1a99ae459d 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -1,6 +1,7 @@ import os import re import numbers +from string import Formatter KEY_PATTERN = re.compile(r"(\{.*?[^{0]*\})") KEY_PADDING_PATTERN = re.compile(r"([^:]+)\S+[><]\S+") @@ -48,16 +49,16 @@ class StringTemplate: self._template = template parts = [] - last_end_idx = 0 - for item in KEY_PATTERN.finditer(template): - start, end = item.span() - if start > last_end_idx: - parts.append(template[last_end_idx:start]) - parts.append(FormattingPart(template[start:end])) - last_end_idx = end + formatter = Formatter() - if last_end_idx < len(template): - parts.append(template[last_end_idx:len(template)]) + for item in formatter.parse(template): + literal_text, field_name, format_spec, conversion = item + if literal_text: + parts.append(literal_text) + if field_name: + parts.append( + FormattingPart(field_name, format_spec, conversion) + ) new_parts = [] for part in parts: From e4875cc5096a5381861b45f45c28eb6c2a68215e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:37:34 +0100 Subject: [PATCH 289/546] fill FormattingPart init --- client/ayon_core/lib/path_templates.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 1a99ae459d..3871b97849 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -437,8 +437,21 @@ class FormattingPart: Args: template(str): String containing the formatting key. """ - def __init__(self, template): - self._template = template + def __init__(self, field_name, format_spec, conversion): + format_spec_v = "" + if format_spec: + format_spec_v = f":{format_spec}" + conversion_v = "" + if conversion: + conversion_v = f"!{conversion}" + + self._field_name = field_name + self._format_spec = format_spec_v + self._conversion = conversion_v + + template_base = f"{field_name}{format_spec_v}{conversion_v}" + self._template_base = template_base + self._template = f"{{{template_base}}}" @property def template(self): From d15148f001f17f4488119c402a54005be34e0d38 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 21 Nov 2024 18:38:49 +0100 Subject: [PATCH 290/546] support list in StringTemplate --- client/ayon_core/lib/path_templates.py | 122 +++++++++++++++++-------- 1 file changed, 82 insertions(+), 40 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 3871b97849..9b545f2851 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -1,10 +1,10 @@ import os import re +import copy import numbers +from typing import List from string import Formatter -KEY_PATTERN = re.compile(r"(\{.*?[^{0]*\})") -KEY_PADDING_PATTERN = re.compile(r"([^:]+)\S+[><]\S+") SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") @@ -369,11 +369,10 @@ class TemplatePartResult: @staticmethod def split_keys_to_subdicts(values): output = {} + formatter = Formatter() for key, value in values.items(): - key_padding = list(KEY_PADDING_PATTERN.findall(key)) - if key_padding: - key = key_padding[0] - key_subdict = list(SUB_DICT_PATTERN.findall(key)) + _, field_name, _, _ = next(formatter.parse(f"{{{key}}}")) + key_subdict = list(SUB_DICT_PATTERN.findall(field_name)) data = output last_key = key_subdict.pop(-1) for subkey in key_subdict: @@ -502,6 +501,16 @@ class FormattingPart: return False return not queue + @staticmethod + def keys_to_template_base(keys: List[str]): + if not keys: + return None + # Create copy of keys + keys = list(keys) + template_base = keys.pop(0) + joined_keys = "".join([f"[{key}]" for key in keys]) + return f"{template_base}{joined_keys}" + def format(self, data, result): """Format the formattings string. @@ -509,7 +518,7 @@ class FormattingPart: data(dict): Data that should be used for formatting. result(TemplatePartResult): Object where result is stored. """ - key = self.template[1:-1] + key = self._template_base if key in result.realy_used_values: result.add_output(result.realy_used_values[key]) return result @@ -521,17 +530,38 @@ class FormattingPart: return result # check if key expects subdictionary keys (e.g. project[name]) - existence_check = key - key_padding = list(KEY_PADDING_PATTERN.findall(existence_check)) - if key_padding: - existence_check = key_padding[0] - key_subdict = list(SUB_DICT_PATTERN.findall(existence_check)) + key_subdict = list(SUB_DICT_PATTERN.findall(self._field_name)) value = data missing_key = False invalid_type = False used_keys = [] + keys_to_value = None + used_value = None + for sub_key in key_subdict: + if isinstance(value, list): + if not sub_key.lstrip("-").isdigit(): + invalid_type = True + break + sub_key = int(sub_key) + if sub_key < 0: + sub_key = len(value) + sub_key + + invalid = 0 > sub_key < len(data) + if invalid: + used_keys.append(sub_key) + missing_key = True + break + + used_keys.append(sub_key) + if keys_to_value is None: + keys_to_value = list(used_keys) + keys_to_value.pop(-1) + used_value = copy.deepcopy(value) + value = value[sub_key] + continue + if ( value is None or (hasattr(value, "items") and sub_key not in value) @@ -547,45 +577,57 @@ class FormattingPart: used_keys.append(sub_key) value = value.get(sub_key) - if missing_key or invalid_type: - if len(used_keys) == 0: - invalid_key = key_subdict[0] - else: - invalid_key = used_keys[0] - for idx, sub_key in enumerate(used_keys): - if idx == 0: - continue - invalid_key += "[{0}]".format(sub_key) + field_name = key_subdict[0] + if used_keys: + field_name = self.keys_to_template_base(used_keys) + if missing_key or invalid_type: if missing_key: - result.add_missing_key(invalid_key) + result.add_missing_key(field_name) elif invalid_type: - result.add_invalid_type(invalid_key, value) + result.add_invalid_type(field_name, value) result.add_output(self.template) return result - if self.validate_value_type(value): - fill_data = {} - first_value = True - for used_key in reversed(used_keys): - if first_value: - first_value = False - fill_data[used_key] = value - else: - _fill_data = {used_key: fill_data} - fill_data = _fill_data - - formatted_value = self.template.format(**fill_data) - result.add_realy_used_value(key, formatted_value) - result.add_used_value(existence_check, formatted_value) - result.add_output(formatted_value) + if not self.validate_value_type(value): + result.add_invalid_type(key, value) + result.add_output(self.template) return result - result.add_invalid_type(key, value) - result.add_output(self.template) + fill_data = root_fill_data = {} + parent_fill_data = None + parent_key = None + fill_value = data + value_filled = False + for used_key in used_keys: + if isinstance(fill_value, list): + parent_fill_data[parent_key] = fill_value + value_filled = True + break + fill_value = fill_value[used_key] + parent_fill_data = fill_data + fill_data = parent_fill_data.setdefault(used_key, {}) + parent_key = used_key + if not value_filled: + parent_fill_data[used_keys[-1]] = value + + template = f"{{{field_name}{self._format_spec}{self._conversion}}}" + formatted_value = template.format(**root_fill_data) + used_key = key + if keys_to_value is not None: + used_key = self.keys_to_template_base(keys_to_value) + + if used_value is None: + if isinstance(value, numbers.Number): + used_value = value + else: + used_value = formatted_value + result.add_realy_used_value(self._field_name, used_value) + result.add_used_value(used_key, used_value) + result.add_output(formatted_value) return result From e625901aebe0bab4a97e11ca4725c95ac29a86dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Nov 2024 16:05:17 +0800 Subject: [PATCH 291/546] big roy's comment - code tweak --- client/ayon_core/tools/sceneinventory/model.py | 6 +++--- .../tools/sceneinventory/models/containers.py | 2 +- client/ayon_core/tools/sceneinventory/view.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 75af957cfa..849d8b8d17 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -132,7 +132,7 @@ class InventoryModel(QtGui.QStandardItemModel): # for debugging or testing, injecting items from outside container_items = self._controller.get_container_items() self._clear_items() - repre_id = set() + repre_ids = set() repre_ids_by_project = collections.defaultdict(set) version_items_by_product_id = collections.defaultdict(dict) repre_info_by_id_by_project = collections.defaultdict(dict) @@ -146,7 +146,7 @@ class InventoryModel(QtGui.QStandardItemModel): # continue project_name = container_item.project_name representation_id = container_item.representation_id - repre_id.add(representation_id) + repre_ids.add(representation_id) repre_ids_by_project[project_name].add(representation_id) item_by_repre_id_by_project_id[project_name][representation_id].add(container_item) @@ -168,7 +168,7 @@ class InventoryModel(QtGui.QStandardItemModel): # SiteSync addon information progress_by_id = self._controller.get_representations_site_progress( - repre_id + repre_ids ) sites_info = self._controller.get_sites_information() site_icons = { diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index dc41bdc8fa..aea94b97ef 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -359,9 +359,9 @@ class ContainersModel: containers_by_id = {} container_items_by_id = {} invalid_ids_mapping = {} + current_project_name = self._controller.get_current_project_name() for container in containers: try: - current_project_name = self._controller.get_current_project_name() item = ContainerItem.from_container_data(current_project_name, container) repre_id = item.representation_id try: diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 025bff6e9f..d4381f55cd 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -755,7 +755,7 @@ class SceneInventoryView(QtWidgets.QTreeView): project_name = container_item.project_name repre_ids_by_project[project_name].add(repre_id) - versions = set() + version_ids = set() repre_info_by_project = collections.defaultdict(dict) version_items_by_product_id_by_project = collections.defaultdict(dict) for project_name, repre_ids in repre_ids_by_project.items(): @@ -784,8 +784,8 @@ class SceneInventoryView(QtWidgets.QTreeView): version_items = list( version_items_by_product_id_by_project[project_name][active_product_id].values() ) - all_versions = {version_item.version for version_item in version_items} - versions.update(all_versions) + version_ids.update(version_item.version for version_item in version_items) + product_ids_by_version = collections.defaultdict(set) for version_items_by_product_id in version_items_by_product_id_by_project.values(): for version_items_by_id in version_items_by_product_id.values(): @@ -797,9 +797,9 @@ class SceneInventoryView(QtWidgets.QTreeView): product_ids_by_version[_prod_version].add( version_item.product_id ) - if version in versions: + if version in version_ids: continue - versions.add(version) + version_ids.add(version) version_items.append(version_item) def version_sorter(item): From 842033ddc65e30e6f5d877be5dc57624d7e8c1b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:55:39 +0100 Subject: [PATCH 292/546] use 'folderPath' to calculate 'hierarchy' --- .../plugins/publish/collect_anatomy_instance_data.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index a0bd57d7dc..abd64ec03d 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -413,14 +413,16 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # Backwards compatible (Deprecated since 24/06/06) or instance.data.get("newAssetPublishing") ): - hierarchy = instance.data["hierarchy"] - anatomy_data["hierarchy"] = hierarchy + folder_path = instance.data["folderPath"] + parents = folder_path.lstrip("/").split("/") + folder_name = parents.pop(-1) parent_name = project_entity["name"] - if hierarchy: - parent_name = hierarchy.split("/")[-1] + hierarchy = "" + if parents: + parent_name = parents[-1] + hierarchy = "/".join(parents) - folder_name = instance.data["folderPath"].split("/")[-1] anatomy_data.update({ "asset": folder_name, "hierarchy": hierarchy, From 7edc759842024453cf60702b1bf3b5c431c96fae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:57:49 +0100 Subject: [PATCH 293/546] remove unused variables --- client/ayon_core/tools/sceneinventory/models/containers.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index aea94b97ef..ad78061468 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -194,19 +194,15 @@ class ContainersModel: self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} - self._container_items_by_project = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} - self._product_ids_by_project = {} def reset(self): self._items_cache = None self._containers_by_id = {} self._container_items_by_id = {} - self._container_items_by_project = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} - self._product_ids_by_project = {} def get_containers(self): self._update_cache() From ef26dc2dc2cb50957710e25c2e7cc9aacad8d7da Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:58:02 +0100 Subject: [PATCH 294/546] added empty lines for readability --- client/ayon_core/tools/sceneinventory/models/containers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index ad78061468..b1cbb38587 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -294,10 +294,12 @@ class ContainersModel: def version_sorted(entity): return entity["version"] + version_entities_by_product_id = { product_id: [] for product_id in missing_ids } + version_entities = list(ayon_api.get_versions( project_name, product_ids=missing_ids, @@ -309,6 +311,7 @@ class ContainersModel: version_entities_by_product_id[product_id].append( version_entity ) + for product_id, version_entities in ( version_entities_by_product_id.items() ): @@ -334,6 +337,7 @@ class ContainersModel: self._version_items_by_product_id[product_id] = ( version_items_by_id ) + return { product_id: dict(self._version_items_by_product_id[product_id]) for product_id in product_ids From 7935ed3284fca091eed45eb13acfd3ea456e0145 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:58:23 +0100 Subject: [PATCH 295/546] site sync expects project name --- client/ayon_core/tools/sceneinventory/control.py | 14 ++++++++++---- .../tools/sceneinventory/models/sitesync.py | 10 +++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 8c02881b82..12dfc72e77 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -124,14 +124,20 @@ class SceneInventoryController: def get_site_provider_icons(self): return self._sitesync_model.get_site_provider_icons() - def get_representations_site_progress(self, representation_ids): + def get_representations_site_progress( + self, project_name, representation_ids + ): return self._sitesync_model.get_representations_site_progress( - representation_ids + project_name, representation_ids ) - def resync_representations(self, representation_ids, site_type): + def resync_representations( + self, project_name, representation_ids, site_type + ): return self._sitesync_model.resync_representations( - representation_ids, site_type + project_name, + representation_ids, + site_type ) # Switch dialog methods diff --git a/client/ayon_core/tools/sceneinventory/models/sitesync.py b/client/ayon_core/tools/sceneinventory/models/sitesync.py index 1a1f08bf02..1738ec2c15 100644 --- a/client/ayon_core/tools/sceneinventory/models/sitesync.py +++ b/client/ayon_core/tools/sceneinventory/models/sitesync.py @@ -54,7 +54,9 @@ class SiteSyncModel: "remote_site_provider": self._get_remote_site_provider() } - def get_representations_site_progress(self, representation_ids): + def get_representations_site_progress( + self, project_name, representation_ids + ): """Get progress of representations sync.""" representation_ids = set(representation_ids) @@ -68,7 +70,6 @@ class SiteSyncModel: if not self.is_sitesync_enabled(): return output - project_name = self._controller.get_current_project_name() sitesync_addon = self._get_sitesync_addon() repre_entities = ayon_api.get_representations( project_name, representation_ids @@ -86,10 +87,13 @@ class SiteSyncModel: return output - def resync_representations(self, representation_ids, site_type): + def resync_representations( + self, project_name, representation_ids, site_type + ): """ Args: + project_name (str): Project name. representation_ids (Iterable[str]): Representation ids. site_type (Literal[active_site, remote_site]): Site type. """ From b25715ee2bfc801486da97259890eb6425e1e35d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:59:47 +0100 Subject: [PATCH 296/546] use project name in site sync calls --- .../ayon_core/tools/sceneinventory/model.py | 13 +++++---- .../tools/sceneinventory/models/sitesync.py | 2 +- client/ayon_core/tools/sceneinventory/view.py | 29 +++++++++++++------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 849d8b8d17..cf9814a8e1 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -132,7 +132,6 @@ class InventoryModel(QtGui.QStandardItemModel): # for debugging or testing, injecting items from outside container_items = self._controller.get_container_items() self._clear_items() - repre_ids = set() repre_ids_by_project = collections.defaultdict(set) version_items_by_product_id = collections.defaultdict(dict) repre_info_by_id_by_project = collections.defaultdict(dict) @@ -146,7 +145,6 @@ class InventoryModel(QtGui.QStandardItemModel): # continue project_name = container_item.project_name representation_id = container_item.representation_id - repre_ids.add(representation_id) repre_ids_by_project[project_name].add(representation_id) item_by_repre_id_by_project_id[project_name][representation_id].add(container_item) @@ -167,9 +165,13 @@ class InventoryModel(QtGui.QStandardItemModel): version_items_by_product_id[project_name] = version_items # SiteSync addon information - progress_by_id = self._controller.get_representations_site_progress( - repre_ids - ) + progress_by_project = {} + for project_name, repre_ids in repre_ids_by_project.items(): + progress_by_id = self._controller.get_representations_site_progress( + project_name, repre_ids + ) + progress_by_project[project_name] = progress_by_id + sites_info = self._controller.get_sites_information() site_icons = { provider: get_qt_icon(icon_def) @@ -207,6 +209,7 @@ class InventoryModel(QtGui.QStandardItemModel): root_item = self.invisibleRootItem() group_items = [] for project_name, items_by_repre_id in item_by_repre_id_by_project_id.items(): + progress_by_id = progress_by_project[project_name] for repre_id, container_items in items_by_repre_id.items(): repre_info = repre_info_by_id_by_project[project_name][repre_id] version_color = None diff --git a/client/ayon_core/tools/sceneinventory/models/sitesync.py b/client/ayon_core/tools/sceneinventory/models/sitesync.py index 1738ec2c15..c8e1ac2cd3 100644 --- a/client/ayon_core/tools/sceneinventory/models/sitesync.py +++ b/client/ayon_core/tools/sceneinventory/models/sitesync.py @@ -103,7 +103,7 @@ class SiteSyncModel: active_site = self._get_active_site() remote_site = self._get_remote_site() progress = self.get_representations_site_progress( - representation_ids + project_name, representation_ids ) for repre_id in representation_ids: repre_progress = progress.get(repre_id) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index d4381f55cd..9bd2d65cd0 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -413,12 +413,13 @@ class SceneInventoryView(QtWidgets.QTreeView): self._handle_sitesync(menu, valid_repre_ids) - def _handle_sitesync(self, menu, repre_ids): + def _handle_sitesync(self, menu, repre_ids_by_project_name): """Adds actions for download/upload when SyncServer is enabled Args: menu (OptionMenu) - repre_ids (list) of object_ids + repre_ids_by_project_name (Dict[str, Set[str]]): Representation + ids by project name. Returns: (OptionMenu) @@ -427,7 +428,7 @@ class SceneInventoryView(QtWidgets.QTreeView): if not self._controller.is_sitesync_enabled(): return - if not repre_ids: + if not repre_ids_by_project_name: return menu.addSeparator() @@ -439,7 +440,10 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) download_active_action.triggered.connect( - lambda: self._add_sites(repre_ids, "active_site")) + lambda: self._add_sites( + repre_ids_by_project_name, "active_site" + ) + ) upload_icon = qtawesome.icon("fa.upload", color=DEFAULT_COLOR) upload_remote_action = QtWidgets.QAction( @@ -448,23 +452,30 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) upload_remote_action.triggered.connect( - lambda: self._add_sites(repre_ids, "remote_site")) + lambda: self._add_sites( + repre_ids_by_project_name, "remote_site" + ) + ) menu.addAction(download_active_action) menu.addAction(upload_remote_action) - def _add_sites(self, repre_ids, site_type): + def _add_sites(self, repre_ids_by_project_name, site_type): """(Re)sync all 'repre_ids' to specific site. It checks if opposite site has fully available content to limit accidents. (ReSync active when no remote >> losing active content) Args: - repre_ids (list) + repre_ids_by_project_name (Dict[str, Set[str]]): Representation + ids by project name. site_type (Literal[active_site, remote_site]): Site type. - """ - self._controller.resync_representations(repre_ids, site_type) + """ + for project_name, repre_ids in repre_ids_by_project_name.items(): + self._controller.resync_representations( + project_name, repre_ids, site_type + ) self.data_changed.emit() From a30698eb4b12381ec7c01f7ca1c98ae0e12fab3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:02:49 +0100 Subject: [PATCH 297/546] refactor view codebase --- client/ayon_core/tools/sceneinventory/view.py | 105 ++++++++++-------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 9bd2d65cd0..b3322ffc60 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -198,34 +198,41 @@ class SceneInventoryView(QtWidgets.QTreeView): project_name = container_item.project_name repre_ids_by_project[project_name].add(repre_id) - repre_info_by_project = collections.defaultdict(dict) + repre_info_by_project = {} + repre_ids_by_project_name = {} + version_ids_by_project = {} + product_ids_by_project = {} for project_name, repre_ids in repre_ids_by_project.items(): - repre_info = self._controller.get_representation_info_items( - project_name, repre_ids) - repre_info_by_project[project_name].update(repre_info) + repres_info = self._controller.get_representation_info_items( + project_name, repre_ids + ) + + repre_info_by_project[project_name] = repres_info + repre_ids = set() + version_ids = set() + product_ids = set() + for repre_id, repre_info in repres_info.items(): + if not repre_info.is_valid: + continue + repre_ids.add(repre_id) + version_ids.add(repre_info.version_id) + product_ids.add(repre_info.product_id) + + repre_ids_by_project_name[project_name] = repre_ids + version_ids_by_project[project_name] = version_ids + product_ids_by_project[project_name] = product_ids + # Exclude items that are "NOT FOUND" since setting versions, updating # and removal won't work for those items. filtered_items = [] - version_ids = set() - valid_repre_ids = set() - product_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): project_name = container_item.project_name - repre_info_by_id = repre_info_by_project.get(project_name) repre_id = container_item.representation_id - all_valid_repre_ids = { - repre_id - for repre_id, repre_info in repre_info_by_id.items() - if repre_info.is_valid - } - valid_repre_ids.update(all_valid_repre_ids) - + repre_info_by_id = repre_info_by_project.get(project_name, {}) repre_info = repre_info_by_id.get(repre_id) if repre_info and repre_info.is_valid: filtered_items.append(container_item) - version_ids.add(repre_info.version_id) - product_id = repre_info.product_id - product_ids_by_project[project_name].add(product_id) + # remove remove_icon = qtawesome.icon("fa.remove", color=DEFAULT_COLOR) remove_action = QtWidgets.QAction(remove_icon, "Remove items", menu) @@ -236,18 +243,23 @@ class SceneInventoryView(QtWidgets.QTreeView): # Keep remove action for invalid items menu.addAction(remove_action) return - version_items_by_product_id_by_project = collections.defaultdict(dict) - for project_name, product_ids in product_ids_by_project.items(): - version_items = self._controller.get_version_items( + + version_items_by_project = { + project_name: self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id_by_project[project_name] = version_items + for project_name, product_ids in product_ids_by_project.items() + } + has_outdated = False has_loaded_hero_versions = False has_available_hero_version = False has_outdated_approved = False last_version_by_product_id = {} - for version_items_by_product_id in version_items_by_product_id_by_project.values(): + for project_name, version_items_by_product_id in ( + version_items_by_project.items() + ): + version_ids = version_ids_by_project[project_name] for product_id, version_items_by_id in ( version_items_by_product_id.items() ): @@ -411,7 +423,7 @@ class SceneInventoryView(QtWidgets.QTreeView): menu.addAction(remove_action) - self._handle_sitesync(menu, valid_repre_ids) + self._handle_sitesync(menu, repre_ids_by_project_name) def _handle_sitesync(self, menu, repre_ids_by_project_name): """Adds actions for download/upload when SyncServer is enabled @@ -979,44 +991,47 @@ class SceneInventoryView(QtWidgets.QTreeView): repre_ids_by_project[project_name].add(repre_id) # Get representation info items by ID - repre_info_by_project = collections.defaultdict(dict) + repres_info_by_project = {} + version_items_by_project = {} for project_name, repre_ids in repre_ids_by_project.items(): - repre_info = self._controller.get_representation_info_items( - project_name, repre_ids) - repre_info_by_project[project_name].update(repre_info) + repre_info_by_id = self._controller.get_representation_info_items( + project_name, repre_ids + ) + repres_info_by_project[project_name] = repre_info_by_id - version_items_by_product_id_by_project = collections.defaultdict(dict) - for container_item in containers_items_by_id.values(): - project_name = container_item.project_name - repre_info_by_id = repre_info_by_project.get(project_name) - repre_id = container_item.representation_id - repre_info = repre_info_by_id.get(repre_id) product_ids = { repre_info.product_id for repre_info in repre_info_by_id.values() if repre_info.is_valid } - version_items = self._controller.get_version_items( + version_items_by_product_id = self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id_by_project[project_name] = version_items + version_items_by_project[project_name] = ( + version_items_by_product_id + ) update_containers = [] update_versions = [] - for item_id, container_item in containers_items_by_id.items(): - repre_id = container_item.representation_id + for container_item in containers_items_by_id.values(): project_name = container_item.project_name + repre_id = container_item.representation_id + + repre_info_by_id = repres_info_by_project[project_name] repre_info = repre_info_by_id[repre_id] - product_id = repre_info.product_id - version_items_id = ( - version_items_by_product_id_by_project[project_name][product_id] + + version_items_by_product_id = ( + version_items_by_project[project_name] ) - version_item = version_items_id.get(repre_info.version_id, {}) + product_id = repre_info.product_id + version_items_by_id = version_items_by_product_id[product_id] + version_item = version_items_by_id.get(repre_info.version_id, {}) if not version_item or not version_item.is_hero: continue + version = abs(version_item.version) version_found = False - for version_item in version_items_id.values(): + for version_item in version_items_by_id.values(): if version_item.is_hero: continue if version_item.version == version: @@ -1029,8 +1044,8 @@ class SceneInventoryView(QtWidgets.QTreeView): update_containers.append(container_item.item_id) update_versions.append(version) - # Specify version per item to update to - self._update_containers(update_containers, update_versions) + # Specify version per item to update to + self._update_containers(update_containers, update_versions) def _update_containers(self, item_ids, versions): """Helper to update items to given version (or version per item) From 562f2edacee460709963213c1aeae155416c097f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:22:27 +0100 Subject: [PATCH 298/546] site sync is fully project specific --- .../ayon_core/tools/sceneinventory/control.py | 4 +- .../ayon_core/tools/sceneinventory/model.py | 52 +++++--- .../tools/sceneinventory/models/sitesync.py | 121 +++++++++--------- 3 files changed, 98 insertions(+), 79 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 12dfc72e77..4f23b8e942 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -118,8 +118,8 @@ class SceneInventoryController: def is_sitesync_enabled(self): return self._sitesync_model.is_sitesync_enabled() - def get_sites_information(self): - return self._sitesync_model.get_sites_information() + def get_sites_information(self, project_name): + return self._sitesync_model.get_sites_information(project_name) def get_site_provider_icons(self): return self._sitesync_model.get_site_provider_icons() diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index cf9814a8e1..95272470aa 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -133,10 +133,10 @@ class InventoryModel(QtGui.QStandardItemModel): container_items = self._controller.get_container_items() self._clear_items() repre_ids_by_project = collections.defaultdict(set) - version_items_by_product_id = collections.defaultdict(dict) + version_items_by_project = collections.defaultdict(dict) repre_info_by_id_by_project = collections.defaultdict(dict) - item_by_repre_id_by_project_id = collections.defaultdict( - lambda: collections.defaultdict(set)) + item_by_repre_id_by_project = collections.defaultdict( + lambda: collections.defaultdict(list)) for container_item in container_items: # if ( # selected is not None @@ -146,7 +146,11 @@ class InventoryModel(QtGui.QStandardItemModel): project_name = container_item.project_name representation_id = container_item.representation_id repre_ids_by_project[project_name].add(representation_id) - item_by_repre_id_by_project_id[project_name][representation_id].add(container_item) + ( + item_by_repre_id_by_project + [project_name] + [representation_id] + ).append(container_item) for project_name, representation_ids in repre_ids_by_project.items(): repre_info = self._controller.get_representation_info_items( @@ -162,17 +166,20 @@ class InventoryModel(QtGui.QStandardItemModel): version_items = self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id[project_name] = version_items + version_items_by_project[project_name] = version_items # SiteSync addon information - progress_by_project = {} - for project_name, repre_ids in repre_ids_by_project.items(): - progress_by_id = self._controller.get_representations_site_progress( + progress_by_project = { + project_name: self._controller.get_representations_site_progress( project_name, repre_ids ) - progress_by_project[project_name] = progress_by_id + for project_name, repre_ids in repre_ids_by_project.items() + } - sites_info = self._controller.get_sites_information() + sites_info_by_project_name = { + project_name: self._controller.get_sites_information(project_name) + for project_name in repre_ids_by_project.keys() + } site_icons = { provider: get_qt_icon(icon_def) for provider, icon_def in ( @@ -203,15 +210,26 @@ class InventoryModel(QtGui.QStandardItemModel): group_item_font = QtGui.QFont() group_item_font.setBold(True) - active_site_icon = site_icons.get(sites_info["active_site_provider"]) - remote_site_icon = site_icons.get(sites_info["remote_site_provider"]) - root_item = self.invisibleRootItem() group_items = [] - for project_name, items_by_repre_id in item_by_repre_id_by_project_id.items(): + for project_name, items_by_repre_id in ( + item_by_repre_id_by_project.items() + ): + sites_info = sites_info_by_project_name[project_name] + active_site_icon = site_icons.get( + sites_info["active_site_provider"] + ) + remote_site_icon = site_icons.get( + sites_info["remote_site_provider"] + ) + progress_by_id = progress_by_project[project_name] + repre_info_by_id = repre_info_by_id_by_project[project_name] + version_items_by_product_id = ( + version_items_by_project[project_name] + ) for repre_id, container_items in items_by_repre_id.items(): - repre_info = repre_info_by_id_by_project[project_name][repre_id] + repre_info = repre_info_by_id[repre_id] version_color = None if not repre_info.is_valid: version_label = "N/A" @@ -230,7 +248,7 @@ class InventoryModel(QtGui.QStandardItemModel): item_icon = valid_item_icon version_items = ( - version_items_by_product_id[project_name][repre_info.product_id] + version_items_by_product_id[repre_info.product_id] ) version_item = version_items[repre_info.version_id] version_label = format_version(version_item.version) @@ -266,8 +284,6 @@ class InventoryModel(QtGui.QStandardItemModel): item.setData(True, IS_CONTAINER_ITEM_ROLE) item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) container_model_items.append(item) - if not container_model_items: - continue progress = progress_by_id[repre_id] active_site_progress = "{}%".format( diff --git a/client/ayon_core/tools/sceneinventory/models/sitesync.py b/client/ayon_core/tools/sceneinventory/models/sitesync.py index c8e1ac2cd3..546d2b15c0 100644 --- a/client/ayon_core/tools/sceneinventory/models/sitesync.py +++ b/client/ayon_core/tools/sceneinventory/models/sitesync.py @@ -11,18 +11,18 @@ class SiteSyncModel: self._sitesync_addon = NOT_SET self._sitesync_enabled = None - self._active_site = NOT_SET - self._remote_site = NOT_SET - self._active_site_provider = NOT_SET - self._remote_site_provider = NOT_SET + self._active_site = {} + self._remote_site = {} + self._active_site_provider = {} + self._remote_site_provider = {} def reset(self): self._sitesync_addon = NOT_SET self._sitesync_enabled = None - self._active_site = NOT_SET - self._remote_site = NOT_SET - self._active_site_provider = NOT_SET - self._remote_site_provider = NOT_SET + self._active_site = {} + self._remote_site = {} + self._active_site_provider = {} + self._remote_site_provider = {} def is_sitesync_enabled(self): """Site sync is enabled. @@ -46,12 +46,16 @@ class SiteSyncModel: sitesync_addon = self._get_sitesync_addon() return sitesync_addon.get_site_icons() - def get_sites_information(self): + def get_sites_information(self, project_name): return { - "active_site": self._get_active_site(), - "active_site_provider": self._get_active_site_provider(), - "remote_site": self._get_remote_site(), - "remote_site_provider": self._get_remote_site_provider() + "active_site": self._get_active_site(project_name), + "remote_site": self._get_remote_site(project_name), + "active_site_provider": self._get_active_site_provider( + project_name + ), + "remote_site_provider": self._get_remote_site_provider( + project_name + ) } def get_representations_site_progress( @@ -74,8 +78,8 @@ class SiteSyncModel: repre_entities = ayon_api.get_representations( project_name, representation_ids ) - active_site = self._get_active_site() - remote_site = self._get_remote_site() + active_site = self._get_active_site(project_name) + remote_site = self._get_remote_site(project_name) for repre_entity in repre_entities: repre_output = output[repre_entity["id"]] @@ -97,11 +101,9 @@ class SiteSyncModel: representation_ids (Iterable[str]): Representation ids. site_type (Literal[active_site, remote_site]): Site type. """ - - project_name = self._controller.get_current_project_name() sitesync_addon = self._get_sitesync_addon() - active_site = self._get_active_site() - remote_site = self._get_remote_site() + active_site = self._get_active_site(project_name) + remote_site = self._get_remote_site(project_name) progress = self.get_representations_site_progress( project_name, representation_ids ) @@ -136,48 +138,49 @@ class SiteSyncModel: self._sitesync_addon = sitesync_addon self._sitesync_enabled = sync_enabled - def _get_active_site(self): - if self._active_site is NOT_SET: - self._cache_sites() - return self._active_site + def _get_active_site(self, project_name): + if project_name not in self._active_site: + self._cache_sites(project_name) + return self._active_site[project_name] - def _get_remote_site(self): - if self._remote_site is NOT_SET: - self._cache_sites() - return self._remote_site + def _get_remote_site(self, project_name): + if project_name not in self._remote_site: + self._cache_sites(project_name) + return self._remote_site[project_name] - def _get_active_site_provider(self): - if self._active_site_provider is NOT_SET: - self._cache_sites() - return self._active_site_provider + def _get_active_site_provider(self, project_name): + if project_name not in self._active_site_provider: + self._cache_sites(project_name) + return self._active_site_provider[project_name] - def _get_remote_site_provider(self): - if self._remote_site_provider is NOT_SET: - self._cache_sites() - return self._remote_site_provider + def _get_remote_site_provider(self, project_name): + if project_name not in self._remote_site_provider: + self._cache_sites(project_name) + return self._remote_site_provider[project_name] - def _cache_sites(self): - active_site = None - remote_site = None - active_site_provider = None - remote_site_provider = None - if self.is_sitesync_enabled(): - sitesync_addon = self._get_sitesync_addon() - project_name = self._controller.get_current_project_name() - active_site = sitesync_addon.get_active_site(project_name) - remote_site = sitesync_addon.get_remote_site(project_name) - active_site_provider = "studio" - remote_site_provider = "studio" - if active_site != "studio": - active_site_provider = sitesync_addon.get_provider_for_site( - project_name, active_site - ) - if remote_site != "studio": - remote_site_provider = sitesync_addon.get_provider_for_site( - project_name, remote_site - ) + def _cache_sites(self, project_name): + self._active_site[project_name] = None + self._remote_site[project_name] = None + self._active_site_provider[project_name] = None + self._remote_site_provider[project_name] = None + if not self.is_sitesync_enabled(): + return - self._active_site = active_site - self._remote_site = remote_site - self._active_site_provider = active_site_provider - self._remote_site_provider = remote_site_provider + sitesync_addon = self._get_sitesync_addon() + active_site = sitesync_addon.get_active_site(project_name) + remote_site = sitesync_addon.get_remote_site(project_name) + active_site_provider = "studio" + remote_site_provider = "studio" + if active_site != "studio": + active_site_provider = sitesync_addon.get_provider_for_site( + project_name, active_site + ) + if remote_site != "studio": + remote_site_provider = sitesync_addon.get_provider_for_site( + project_name, remote_site + ) + + self._active_site[project_name] = active_site + self._remote_site[project_name] = remote_site + self._active_site_provider[project_name] = active_site_provider + self._remote_site_provider[project_name] = remote_site_provider From eea31b676d91c7ae746f7f1d2417737273c6cb05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:25:46 +0100 Subject: [PATCH 299/546] don't slow down project name getter --- client/ayon_core/pipeline/load/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index a6c5f0ce1f..de8e1676e7 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -465,7 +465,9 @@ def update_container(container, version=-1): from ayon_core.pipeline import get_current_project_name # Compute the different version from 'representation' - project_name = container.get("project_name", get_current_project_name()) + project_name = container.get("project_name") + if project_name is None: + project_name = get_current_project_name() repre_id = container["representation"] if not _is_valid_representation_id(repre_id): raise ValueError( @@ -588,7 +590,9 @@ def switch_container(container, representation, loader_plugin=None): ) # Get the new representation to switch to - project_name = container.get("project_name", get_current_project_name()) + project_name = container.get("project_name") + if project_name is None: + project_name = get_current_project_name() context = get_representation_context( project_name, representation["id"] From e21c7a157e5009e521d804fb0621bf55d750cba2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 18:46:56 +0100 Subject: [PATCH 300/546] use project name to get correct status icons --- .../ayon_core/tools/sceneinventory/control.py | 5 +- .../ayon_core/tools/sceneinventory/model.py | 48 ++++++++++++------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/control.py b/client/ayon_core/tools/sceneinventory/control.py index 4f23b8e942..60d9bc77a9 100644 --- a/client/ayon_core/tools/sceneinventory/control.py +++ b/client/ayon_core/tools/sceneinventory/control.py @@ -86,8 +86,9 @@ class SceneInventoryController: self._current_folder_set = True return self._current_folder_id - def get_project_status_items(self): - project_name = self.get_current_project_name() + def get_project_status_items(self, project_name=None): + if project_name is None: + project_name = self.get_current_project_name() return self._projects_model.get_project_status_items( project_name, None ) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 95272470aa..79af0e5cf5 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -121,8 +121,8 @@ class InventoryModel(QtGui.QStandardItemModel): self._default_icon_color = get_default_entity_icon_color() - self._last_project_statuses = {} - self._last_status_icons_by_name = {} + self._last_project_statuses = collections.defaultdict(dict) + self._last_status_icons_by_name = collections.defaultdict(dict) def outdated(self, item): return item.get("isOutdated", True) @@ -131,7 +131,10 @@ class InventoryModel(QtGui.QStandardItemModel): """Refresh the model""" # for debugging or testing, injecting items from outside container_items = self._controller.get_container_items() + self._clear_items() + + project_names = set() repre_ids_by_project = collections.defaultdict(set) version_items_by_project = collections.defaultdict(dict) repre_info_by_id_by_project = collections.defaultdict(dict) @@ -145,6 +148,7 @@ class InventoryModel(QtGui.QStandardItemModel): # continue project_name = container_item.project_name representation_id = container_item.representation_id + project_names.add(project_name) repre_ids_by_project[project_name].add(representation_id) ( item_by_repre_id_by_project @@ -178,7 +182,7 @@ class InventoryModel(QtGui.QStandardItemModel): sites_info_by_project_name = { project_name: self._controller.get_sites_information(project_name) - for project_name in repre_ids_by_project.keys() + for project_name in project_names } site_icons = { provider: get_qt_icon(icon_def) @@ -186,11 +190,17 @@ class InventoryModel(QtGui.QStandardItemModel): self._controller.get_site_provider_icons().items() ) } - self._last_project_statuses = { - status_item.name: status_item - for status_item in self._controller.get_project_status_items() - } - self._last_status_icons_by_name = {} + last_project_statuses = collections.defaultdict(dict) + for project_name in project_names: + status_items_by_name = { + status_item.name: status_item + for status_item in self._controller.get_project_status_items( + project_name + ) + } + last_project_statuses[project_name] = status_items_by_name + self._last_project_statuses = last_project_statuses + self._last_status_icons_by_name = collections.defaultdict(dict) group_item_icon = qtawesome.icon( "fa.folder", color=self._default_icon_color @@ -258,9 +268,9 @@ class InventoryModel(QtGui.QStandardItemModel): version_color = self.OUTDATED_COLOR status_name = version_item.status - status_color, status_short, status_icon = self._get_status_data( - status_name - ) + ( + status_color, status_short, status_icon + ) = self._get_status_data(project_name, status_name) repre_name = ( repre_info.representation_name or "" @@ -392,17 +402,21 @@ class InventoryModel(QtGui.QStandardItemModel): root_item = self.invisibleRootItem() root_item.removeRows(0, root_item.rowCount()) - def _get_status_data(self, status_name): - status_item = self._last_project_statuses.get(status_name) - status_icon = self._get_status_icon(status_name, status_item) + def _get_status_data(self, project_name, status_name): + status_item = self._last_project_statuses[project_name].get( + status_name + ) + status_icon = self._get_status_icon( + project_name, status_name, status_item + ) status_color = status_short = None if status_item is not None: status_color = status_item.color status_short = status_item.short return status_color, status_short, status_icon - def _get_status_icon(self, status_name, status_item): - icon = self._last_status_icons_by_name.get(status_name) + def _get_status_icon(self, project_name, status_name, status_item): + icon = self._last_status_icons_by_name[project_name].get(status_name) if icon is not None: return icon @@ -415,7 +429,7 @@ class InventoryModel(QtGui.QStandardItemModel): }) if icon is None: icon = QtGui.QIcon() - self._last_status_icons_by_name[status_name] = icon + self._last_status_icons_by_name[project_name][status_name] = icon return icon From 87907b550b1410a6b6912f611287123e310e7f33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:05:29 +0100 Subject: [PATCH 301/546] fix switch version --- client/ayon_core/tools/sceneinventory/view.py | 92 +++++++++++-------- 1 file changed, 53 insertions(+), 39 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index b3322ffc60..9112db2ef3 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -772,60 +772,71 @@ class SceneInventoryView(QtWidgets.QTreeView): container_items_by_id = self._controller.get_container_items_by_id( item_ids ) + project_names = set() repre_ids_by_project = collections.defaultdict(set) for container_item in container_items_by_id.values(): repre_id = container_item.representation_id project_name = container_item.project_name + project_names.add(project_name) repre_ids_by_project[project_name].add(repre_id) - version_ids = set() - repre_info_by_project = collections.defaultdict(dict) - version_items_by_product_id_by_project = collections.defaultdict(dict) + active_project_name = None + active_repre_info = None + repre_info_by_project = {} + version_items_by_project = {} for project_name, repre_ids in repre_ids_by_project.items(): - repre_info = self._controller.get_representation_info_items( + repres_info = self._controller.get_representation_info_items( project_name, repre_ids ) - repre_info_by_project[project_name].update(repre_info) + if active_repre_info is None: + active_project_name = project_name + active_repre_info = repres_info.get(active_repre_id) - for container_item in container_items_by_id.values(): - project_name = container_item.project_name - repre_info_by_id = repre_info_by_project.get(project_name) - repre_id = container_item.representation_id - repre_info = repre_info_by_id.get(repre_id) product_ids = { repre_info.product_id - for repre_info in repre_info_by_id.values() + for repre_info in repres_info.values() if repre_info.is_valid } - version_items = self._controller.get_version_items( + version_items_by_product_id = self._controller.get_version_items( project_name, product_ids ) - version_items_by_product_id_by_project[project_name] = version_items - active_repre_info = repre_info_by_id[active_repre_id] - active_version_id = active_repre_info.version_id - active_product_id = active_repre_info.product_id - version_items = list( - version_items_by_product_id_by_project[project_name][active_product_id].values() + + repre_info_by_project[project_name] = repres_info + version_items_by_project[project_name] = version_items_by_product_id + + active_version_id = active_repre_info.version_id + active_product_id = active_repre_info.product_id + + versions = set() + product_ids = set() + version_items = [] + product_ids_by_version_by_project = {} + for project_name, version_items_by_product_id in ( + version_items_by_project.items() + ): + product_ids_by_version = collections.defaultdict(set) + product_ids_by_version_by_project[project_name] = ( + product_ids_by_version ) - version_ids.update(version_item.version for version_item in version_items) + versions |= { + version_item.version + for version_item in version_items_by_product_id.values() + } + for version_item in version_items_by_product_id.values(): + version = version_item.version + _prod_version = version + if _prod_version < 0: + _prod_version = -1 + product_ids_by_version[_prod_version].add( + version_item.product_id + ) + product_ids.add(version_item.product_id) + if version in versions: + continue + versions.add(version) + version_items.append((project_name, version_item)) - product_ids_by_version = collections.defaultdict(set) - for version_items_by_product_id in version_items_by_product_id_by_project.values(): - for version_items_by_id in version_items_by_product_id.values(): - for version_item in version_items_by_id.values(): - version = version_item.version - _prod_version = version - if _prod_version < 0: - _prod_version = -1 - product_ids_by_version[_prod_version].add( - version_item.product_id - ) - if version in version_ids: - continue - version_ids.add(version) - version_items.append(version_item) - - def version_sorter(item): + def version_sorter(_, item): hero_value = 0 i_version = item.version if i_version < 0: @@ -844,7 +855,8 @@ class SceneInventoryView(QtWidgets.QTreeView): version_options = [] active_version_idx = 0 - for idx, version_item in enumerate(version_items): + for idx, item in enumerate(version_items): + project_name, version_item = item version = version_item.version label = format_version(version) if version_item.version_id == active_version_id: @@ -884,11 +896,13 @@ class SceneInventoryView(QtWidgets.QTreeView): product_version = -1 version = HeroVersionType(version) - product_ids = product_ids_by_version[product_version] - filtered_item_ids = set() for container_item in container_items_by_id.values(): project_name = container_item.project_name + product_ids_by_version = ( + product_ids_by_version_by_project[project_name] + ) + product_ids = product_ids_by_version[product_version] repre_id = container_item.representation_id repre_info = repre_info_by_project[project_name][repre_id] if repre_info.product_id in product_ids: From b28f4b0ff1ea9662c169f508302659f459352075 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:10:05 +0100 Subject: [PATCH 302/546] comment out unused variables --- client/ayon_core/tools/sceneinventory/view.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 9112db2ef3..5892e4f983 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -780,7 +780,7 @@ class SceneInventoryView(QtWidgets.QTreeView): project_names.add(project_name) repre_ids_by_project[project_name].add(repre_id) - active_project_name = None + # active_project_name = None active_repre_info = None repre_info_by_project = {} version_items_by_project = {} @@ -789,7 +789,7 @@ class SceneInventoryView(QtWidgets.QTreeView): project_name, repre_ids ) if active_repre_info is None: - active_project_name = project_name + # active_project_name = project_name active_repre_info = repres_info.get(active_repre_id) product_ids = { @@ -805,7 +805,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_items_by_project[project_name] = version_items_by_product_id active_version_id = active_repre_info.version_id - active_product_id = active_repre_info.product_id + # active_product_id = active_repre_info.product_id versions = set() product_ids = set() From 207b1961a441b4d13df7eac7e80faf39b4468ace Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:35:21 +0100 Subject: [PATCH 303/546] support all errors in ruff linter --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index f2d09d925d..d09fabf8b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ target-version = "py39" [tool.ruff.lint] # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. -select = ["E4", "E7", "E9", "F", "W"] +select = ["E", "F", "W"] ignore = [] # Allow fix for all enabled rules (when `--fix`) is provided. From d30d5b541994a2243d41252a44459d39d0696123 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:37:05 +0100 Subject: [PATCH 304/546] fix line lengths --- client/ayon_core/addon/base.py | 4 +-- client/ayon_core/cli.py | 3 +- client/ayon_core/lib/attribute_definitions.py | 4 ++- client/ayon_core/pipeline/create/context.py | 14 ++++++--- .../ayon_core/pipeline/create/product_name.py | 6 +++- client/ayon_core/pipeline/editorial.py | 13 ++++++-- .../pipeline/farm/pyblish_functions.py | 20 +++++++++--- .../plugins/publish/collect_hierarchy.py | 3 +- .../plugins/publish/extract_otio_review.py | 31 ++++++++++++------- .../publish/validate_unique_subsets.py | 10 +++--- client/ayon_core/scripts/otio_burnin.py | 17 ++++++---- client/ayon_core/settings/lib.py | 3 +- client/ayon_core/tools/creator/widgets.py | 4 ++- .../tools/launcher/models/actions.py | 6 ++-- .../tools/loader/ui/_multicombobox.py | 6 +++- .../tools/loader/ui/products_model.py | 6 ++-- .../publisher/widgets/overview_widget.py | 4 ++- .../publisher/widgets/product_context.py | 6 ++-- .../tools/publisher/widgets/tasks_model.py | 4 +-- client/ayon_core/tools/publisher/window.py | 6 +++- .../tools/sceneinventory/models/containers.py | 4 +-- client/ayon_core/tools/utils/lib.py | 7 +++-- server/settings/publish_plugins.py | 21 +++++++++---- server/settings/tools.py | 8 +++-- .../editorial/test_extract_otio_review.py | 31 +++++++++++-------- 25 files changed, 159 insertions(+), 82 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 982626ad9d..364a84cb7b 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -535,8 +535,8 @@ class AYONAddon(ABC): Implementation of this method is optional. Note: - The logic can be similar to logic in tray, but tray does not require - to be logged in. + The logic can be similar to logic in tray, but tray does not + require to be logged in. Args: process_context (ProcessContext): Context of child diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index b80b243db2..6b4a1f824f 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -146,7 +146,8 @@ def publish_report_viewer(): @main_cli.command() @click.argument("output_path") @click.option("--project", help="Define project context") -@click.option("--folder", help="Define folder in project (project must be set)") +@click.option( + "--folder", help="Define folder in project (project must be set)") @click.option( "--strict", is_flag=True, diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e1381944f6..e8327a45b6 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -616,7 +616,9 @@ class EnumDef(AbstractAttrDef): return data @staticmethod - def prepare_enum_items(items: "EnumItemsInputType") -> List["EnumItemDict"]: + def prepare_enum_items( + items: "EnumItemsInputType" + ) -> List["EnumItemDict"]: """Convert items to unified structure. Output is a list where each item is dictionary with 'value' diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6bfd64b822..e29971415d 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1283,12 +1283,16 @@ class CreateContext: @contextmanager def bulk_pre_create_attr_defs_change(self, sender=None): - with self._bulk_context("pre_create_attrs_change", sender) as bulk_info: + with self._bulk_context( + "pre_create_attrs_change", sender + ) as bulk_info: yield bulk_info @contextmanager def bulk_create_attr_defs_change(self, sender=None): - with self._bulk_context("create_attrs_change", sender) as bulk_info: + with self._bulk_context( + "create_attrs_change", sender + ) as bulk_info: yield bulk_info @contextmanager @@ -1946,9 +1950,9 @@ class CreateContext: creator are just removed from context. Args: - instances (List[CreatedInstance]): Instances that should be removed. - Remove logic is done using creator, which may require to - do other cleanup than just remove instance from context. + instances (List[CreatedInstance]): Instances that should be + removed. Remove logic is done using creator, which may require + to do other cleanup than just remove instance from context. sender (Optional[str]): Sender of the event. """ diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index eaeef6500e..0daec8a7ad 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,5 +1,9 @@ import ayon_api -from ayon_core.lib import StringTemplate, filter_profiles, prepare_template_data +from ayon_core.lib import ( + StringTemplate, + filter_profiles, + prepare_template_data, +) from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index a49a981d2a..2928ef5f63 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -222,6 +222,9 @@ def remap_range_on_file_sequence(otio_clip, in_out_range): source_range = otio_clip.source_range available_range_rate = available_range.start_time.rate media_in = available_range.start_time.value + available_range_start_frame = ( + available_range.start_time.to_frames() + ) # Temporary. # Some AYON custom OTIO exporter were implemented with relative @@ -230,7 +233,7 @@ def remap_range_on_file_sequence(otio_clip, in_out_range): # while we are updating those. if ( is_clip_from_media_sequence(otio_clip) - and otio_clip.available_range().start_time.to_frames() == media_ref.start_frame + and available_range_start_frame == media_ref.start_frame and source_range.start_time.to_frames() < media_ref.start_frame ): media_in = 0 @@ -303,8 +306,12 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): rounded_av_rate = round(available_range_rate, 2) rounded_src_rate = round(source_range.start_time.rate, 2) if rounded_av_rate != rounded_src_rate: - conformed_src_in = source_range.start_time.rescaled_to(available_range_rate) - conformed_src_duration = source_range.duration.rescaled_to(available_range_rate) + conformed_src_in = source_range.start_time.rescaled_to( + available_range_rate + ) + conformed_src_duration = source_range.duration.rescaled_to( + available_range_rate + ) conformed_source_range = otio.opentime.TimeRange( start_time=conformed_src_in, duration=conformed_src_duration diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 16364a17ee..559561c827 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -8,7 +8,10 @@ import attr import ayon_api import clique from ayon_core.lib import Logger, collect_frames -from ayon_core.pipeline import get_current_project_name, get_representation_path +from ayon_core.pipeline import ( + get_current_project_name, + get_representation_path, +) from ayon_core.pipeline.create import get_product_name from ayon_core.pipeline.farm.patterning import match_aov_pattern from ayon_core.pipeline.publish import KnownPublishError @@ -771,9 +774,14 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, project_settings = instance.context.data.get("project_settings") - use_legacy_product_name = True try: - use_legacy_product_name = project_settings["core"]["tools"]["creator"]["use_legacy_product_names_for_renders"] # noqa: E501 + use_legacy_product_name = ( + project_settings + ["core"] + ["tools"] + ["creator"] + ["use_legacy_product_names_for_renders"] + ) except KeyError: warnings.warn( ("use_legacy_for_renders not found in project settings. " @@ -789,7 +797,9 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, dynamic_data=dynamic_data) else: - product_name, group_name = get_product_name_and_group_from_template( + ( + product_name, group_name + ) = get_product_name_and_group_from_template( task_entity=instance.data["taskEntity"], project_name=instance.context.data["projectName"], host_name=instance.context.data["hostName"], @@ -932,7 +942,7 @@ def _collect_expected_files_for_aov(files): # but we really expect only one collection. # Nothing else make sense. if len(cols) != 1: - raise ValueError("Only one image sequence type is expected.") # noqa: E501 + raise ValueError("Only one image sequence type is expected.") return list(cols[0]) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 00f5c06c0b..266c2e1458 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -43,7 +43,8 @@ class CollectHierarchy(pyblish.api.ContextPlugin): shot_data = { "entity_type": "folder", - # WARNING unless overwritten, default folder type is hardcoded to shot + # WARNING unless overwritten, default folder type is hardcoded + # to shot "folder_type": instance.data.get("folder_type") or "Shot", "tasks": instance.data.get("tasks") or {}, "comments": instance.data.get("comments", []), diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index b222c6efc3..fb9b269258 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -129,26 +129,33 @@ class ExtractOTIOReview( res_data[key] = value break - self.to_width, self.to_height = res_data["width"], res_data["height"] - self.log.debug("> self.to_width x self.to_height: {} x {}".format( - self.to_width, self.to_height - )) + self.to_width, self.to_height = ( + res_data["width"], res_data["height"] + ) + self.log.debug( + "> self.to_width x self.to_height:" + f" {self.to_width} x {self.to_height}" + ) available_range = r_otio_cl.available_range() + available_range_start_frame = ( + available_range.start_time.to_frames() + ) processing_range = None self.actual_fps = available_range.duration.rate start = src_range.start_time.rescaled_to(self.actual_fps) duration = src_range.duration.rescaled_to(self.actual_fps) + src_frame_start = src_range.start_time.to_frames() # Temporary. - # Some AYON custom OTIO exporter were implemented with relative - # source range for image sequence. Following code maintain - # backward-compatibility by adjusting available range + # Some AYON custom OTIO exporter were implemented with + # relative source range for image sequence. Following code + # maintain backward-compatibility by adjusting available range # while we are updating those. if ( is_clip_from_media_sequence(r_otio_cl) - and available_range.start_time.to_frames() == media_ref.start_frame - and src_range.start_time.to_frames() < media_ref.start_frame + and available_range_start_frame == media_ref.start_frame + and src_frame_start < media_ref.start_frame ): available_range = otio.opentime.TimeRange( otio.opentime.RationalTime(0, rate=self.actual_fps), @@ -246,7 +253,8 @@ class ExtractOTIOReview( # Extraction via FFmpeg. else: path = media_ref.target_url - # Set extract range from 0 (FFmpeg ignores embedded timecode). + # Set extract range from 0 (FFmpeg ignores + # embedded timecode). extract_range = otio.opentime.TimeRange( otio.opentime.RationalTime( ( @@ -414,7 +422,8 @@ class ExtractOTIOReview( to defined image sequence format. Args: - sequence (list): input dir path string, collection object, fps in list + sequence (list): input dir path string, collection object, + fps in list. video (list)[optional]: video_path string, otio_range in list gap (int)[optional]: gap duration end_offset (int)[optional]: offset gap frame start in frames diff --git a/client/ayon_core/plugins/publish/validate_unique_subsets.py b/client/ayon_core/plugins/publish/validate_unique_subsets.py index 4badeb8112..4067dd75a5 100644 --- a/client/ayon_core/plugins/publish/validate_unique_subsets.py +++ b/client/ayon_core/plugins/publish/validate_unique_subsets.py @@ -11,8 +11,8 @@ class ValidateProductUniqueness(pyblish.api.ContextPlugin): """Validate all product names are unique. This only validates whether the instances currently set to publish from - the workfile overlap one another for the folder + product they are publishing - to. + the workfile overlap one another for the folder + product they are + publishing to. This does not perform any check against existing publishes in the database since it is allowed to publish into existing products resulting in @@ -72,8 +72,10 @@ class ValidateProductUniqueness(pyblish.api.ContextPlugin): # All is ok return - msg = ("Instance product names {} are not unique. ".format(non_unique) + - "Please remove or rename duplicates.") + msg = ( + f"Instance product names {non_unique} are not unique." + " Please remove or rename duplicates." + ) formatting_data = { "non_unique": ",".join(non_unique) } diff --git a/client/ayon_core/scripts/otio_burnin.py b/client/ayon_core/scripts/otio_burnin.py index 6b132b9a6a..cb72606222 100644 --- a/client/ayon_core/scripts/otio_burnin.py +++ b/client/ayon_core/scripts/otio_burnin.py @@ -79,7 +79,8 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): - Datatypes explanation: string format must be supported by FFmpeg. Examples: "#000000", "0x000000", "black" - must be accesible by ffmpeg = name of registered Font in system or path to font file. + must be accesible by ffmpeg = name of registered Font in system + or path to font file. Examples: "Arial", "C:/Windows/Fonts/arial.ttf" - Possible keys: @@ -87,17 +88,21 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): "bg_opacity" - Opacity of background (box around text) - "bg_color" - Background color - "bg_padding" - Background padding in pixels - - "x_offset" - offsets burnin vertically by entered pixels from border - - "y_offset" - offsets burnin horizontally by entered pixels from border - + "x_offset" - offsets burnin vertically by entered pixels + from border - + "y_offset" - offsets burnin horizontally by entered pixels + from border - - x_offset & y_offset should be set at least to same value as bg_padding!! "font" - Font Family for text - "font_size" - Font size in pixels - "font_color" - Color of text - "frame_offset" - Default start frame - - - required IF start frame is not set when using frames or timecode burnins + - required IF start frame is not set when using frames + or timecode burnins - On initializing class can be set General options through "options_init" arg. - General can be overridden when adding burnin + On initializing class can be set General options through + "options_init" arg. + General options can be overridden when adding burnin. ''' TOP_CENTERED = ffmpeg_burnins.TOP_CENTERED diff --git a/client/ayon_core/settings/lib.py b/client/ayon_core/settings/lib.py index 3126bafd57..aa56fa8326 100644 --- a/client/ayon_core/settings/lib.py +++ b/client/ayon_core/settings/lib.py @@ -190,6 +190,7 @@ def get_current_project_settings(): project_name = os.environ.get("AYON_PROJECT_NAME") if not project_name: raise ValueError( - "Missing context project in environemt variable `AYON_PROJECT_NAME`." + "Missing context project in environment" + " variable `AYON_PROJECT_NAME`." ) return get_project_settings(project_name) diff --git a/client/ayon_core/tools/creator/widgets.py b/client/ayon_core/tools/creator/widgets.py index 96ce899881..bbc6848e6c 100644 --- a/client/ayon_core/tools/creator/widgets.py +++ b/client/ayon_core/tools/creator/widgets.py @@ -217,7 +217,9 @@ class ProductTypeDescriptionWidget(QtWidgets.QWidget): product_type_label = QtWidgets.QLabel(self) product_type_label.setObjectName("CreatorProductTypeLabel") - product_type_label.setAlignment(QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft) + product_type_label.setAlignment( + QtCore.Qt.AlignBottom | QtCore.Qt.AlignLeft + ) help_label = QtWidgets.QLabel(self) help_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 7158c05431..8bd30daffa 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -21,9 +21,9 @@ except ImportError: Application action based on 'ApplicationManager' system. - Handling of applications in launcher is not ideal and should be completely - redone from scratch. This is just a temporary solution to keep backwards - compatibility with AYON launcher. + Handling of applications in launcher is not ideal and should be + completely redone from scratch. This is just a temporary solution + to keep backwards compatibility with AYON launcher. Todos: Move handling of errors to frontend. diff --git a/client/ayon_core/tools/loader/ui/_multicombobox.py b/client/ayon_core/tools/loader/ui/_multicombobox.py index c026952418..9efe57ef0f 100644 --- a/client/ayon_core/tools/loader/ui/_multicombobox.py +++ b/client/ayon_core/tools/loader/ui/_multicombobox.py @@ -517,7 +517,11 @@ class CustomPaintMultiselectComboBox(QtWidgets.QComboBox): def setItemCheckState(self, index, state): self.setItemData(index, state, QtCore.Qt.CheckStateRole) - def set_value(self, values: Optional[Iterable[Any]], role: Optional[int] = None): + def set_value( + self, + values: Optional[Iterable[Any]], + role: Optional[int] = None, + ): if role is None: role = self._value_role diff --git a/client/ayon_core/tools/loader/ui/products_model.py b/client/ayon_core/tools/loader/ui/products_model.py index bc24d4d7f7..3571788134 100644 --- a/client/ayon_core/tools/loader/ui/products_model.py +++ b/client/ayon_core/tools/loader/ui/products_model.py @@ -499,8 +499,10 @@ class ProductsModel(QtGui.QStandardItemModel): version_item.version_id for version_item in last_version_by_product_id.values() } - repre_count_by_version_id = self._controller.get_versions_representation_count( - project_name, version_ids + repre_count_by_version_id = ( + self._controller.get_versions_representation_count( + project_name, version_ids + ) ) sync_availability_by_version_id = ( self._controller.get_version_sync_availability( diff --git a/client/ayon_core/tools/publisher/widgets/overview_widget.py b/client/ayon_core/tools/publisher/widgets/overview_widget.py index a09ee80ed5..c6c3b774f0 100644 --- a/client/ayon_core/tools/publisher/widgets/overview_widget.py +++ b/client/ayon_core/tools/publisher/widgets/overview_widget.py @@ -339,7 +339,9 @@ class OverviewWidget(QtWidgets.QFrame): self._change_visibility_for_state() self._product_content_layout.addWidget(self._create_widget, 7) self._product_content_layout.addWidget(self._product_views_widget, 3) - self._product_content_layout.addWidget(self._product_attributes_wrap, 7) + self._product_content_layout.addWidget( + self._product_attributes_wrap, 7 + ) def _change_visibility_for_state(self): self._create_widget.setVisible( diff --git a/client/ayon_core/tools/publisher/widgets/product_context.py b/client/ayon_core/tools/publisher/widgets/product_context.py index 04c9ca7e56..30b318982b 100644 --- a/client/ayon_core/tools/publisher/widgets/product_context.py +++ b/client/ayon_core/tools/publisher/widgets/product_context.py @@ -214,8 +214,8 @@ class TasksCombobox(QtWidgets.QComboBox): Combobox gives ability to select only from intersection of task names for folder paths in selected instances. - If folder paths in selected instances does not have same tasks then combobox - will be empty. + If folder paths in selected instances does not have same tasks + then combobox will be empty. """ value_changed = QtCore.Signal() @@ -604,7 +604,7 @@ class VariantInputWidget(PlaceholderLineEdit): class GlobalAttrsWidget(QtWidgets.QWidget): - """Global attributes mainly to define context and product name of instances. + """Global attributes to define context and product name of instances. product name is or may be affected on context. Gives abiity to modify context and product name of instance. This change is not autopromoted but diff --git a/client/ayon_core/tools/publisher/widgets/tasks_model.py b/client/ayon_core/tools/publisher/widgets/tasks_model.py index 16a4111f59..8bfa81116a 100644 --- a/client/ayon_core/tools/publisher/widgets/tasks_model.py +++ b/client/ayon_core/tools/publisher/widgets/tasks_model.py @@ -22,8 +22,8 @@ class TasksModel(QtGui.QStandardItemModel): tasks with same names then model is empty too. Args: - controller (AbstractPublisherFrontend): Controller which handles creation and - publishing. + controller (AbstractPublisherFrontend): Controller which handles + creation and publishing. """ def __init__( diff --git a/client/ayon_core/tools/publisher/window.py b/client/ayon_core/tools/publisher/window.py index a912495d4e..ed5b909a55 100644 --- a/client/ayon_core/tools/publisher/window.py +++ b/client/ayon_core/tools/publisher/window.py @@ -998,7 +998,11 @@ class PublisherWindow(QtWidgets.QDialog): new_item["label"] = new_item.pop("creator_label") new_item["identifier"] = new_item.pop("creator_identifier") new_failed_info.append(new_item) - self.add_error_message_dialog(event["title"], new_failed_info, "Creator:") + self.add_error_message_dialog( + event["title"], + new_failed_info, + "Creator:" + ) def _on_convertor_error(self, event): new_failed_info = [] diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 4f3ddf1ded..4280445b60 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -366,8 +366,8 @@ class ContainersModel: try: uuid.UUID(repre_id) except (ValueError, TypeError, AttributeError): - # Fake not existing representation id so container is shown in UI - # but as invalid + # Fake not existing representation id so container + # is shown in UI but as invalid item.representation_id = invalid_ids_mapping.setdefault( repre_id, uuid.uuid4().hex ) diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index 200e281664..4b303c0143 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -556,9 +556,10 @@ class _IconsCache: log.info("Didn't find icon \"{}\"".format(icon_name)) elif used_variant != icon_name: - log.debug("Icon \"{}\" was not found \"{}\" is used instead".format( - icon_name, used_variant - )) + log.debug( + f"Icon \"{icon_name}\" was not found" + f" \"{used_variant}\" is used instead" + ) cls._qtawesome_cache[full_icon_name] = icon return icon diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 16b1f37187..8893b00e23 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -358,7 +358,10 @@ class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): custom_tags: list[str] = SettingsField( default_factory=list, title="Custom Tags", - description="Additional custom tags that will be added to the created representation." + description=( + "Additional custom tags that will be added" + " to the created representation." + ) ) @@ -892,9 +895,11 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=CollectFramesFixDefModel, title="Collect Frames to Fix", ) - CollectUSDLayerContributions: CollectUSDLayerContributionsModel = SettingsField( - default_factory=CollectUSDLayerContributionsModel, - title="Collect USD Layer Contributions", + CollectUSDLayerContributions: CollectUSDLayerContributionsModel = ( + SettingsField( + default_factory=CollectUSDLayerContributionsModel, + title="Collect USD Layer Contributions", + ) ) ValidateEditorialAssetName: ValidateBaseModel = SettingsField( default_factory=ValidateBaseModel, @@ -1214,7 +1219,9 @@ DEFAULT_PUBLISH_VALUES = { "TOP_RIGHT": "{anatomy[version]}", "BOTTOM_LEFT": "{username}", "BOTTOM_CENTERED": "{folder[name]}", - "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "BOTTOM_RIGHT": ( + "{frame_start}-{current_frame}-{frame_end}" + ), "filter": { "families": [], "tags": [] @@ -1240,7 +1247,9 @@ DEFAULT_PUBLISH_VALUES = { "TOP_RIGHT": "{anatomy[version]}", "BOTTOM_LEFT": "{username}", "BOTTOM_CENTERED": "{folder[name]}", - "BOTTOM_RIGHT": "{frame_start}-{current_frame}-{frame_end}", + "BOTTOM_RIGHT": ( + "{frame_start}-{current_frame}-{frame_end}" + ), "filter": { "families": [], "tags": [] diff --git a/server/settings/tools.py b/server/settings/tools.py index a2785c1edf..96851be1da 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -83,8 +83,8 @@ class CreatorToolModel(BaseSettingsModel): filter_creator_profiles: list[FilterCreatorProfile] = SettingsField( default_factory=list, title="Filter creator profiles", - description="Allowed list of creator labels that will be only shown if " - "profile matches context." + description="Allowed list of creator labels that will be only shown" + " if profile matches context." ) @validator("product_types_smart_select") @@ -426,7 +426,9 @@ DEFAULT_TOOLS_VALUES = { ], "task_types": [], "tasks": [], - "template": "{product[type]}{Task[name]}_{Renderlayer}_{Renderpass}" + "template": ( + "{product[type]}{Task[name]}_{Renderlayer}_{Renderpass}" + ) }, { "product_types": [ diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index ea31e1a260..8b1c9da30e 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -130,19 +130,20 @@ def test_image_sequence_and_handles_out_of_range(): expected = [ # 5 head black frames generated from gap (991-995) - "/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune " - "stillimage -start_number 991 C:/result/output.%03d.jpg", + "/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720" + " -tune stillimage -start_number 991 C:/result/output.%03d.jpg", # 9 tail back frames generated from gap (1097-1105) - "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune " - "stillimage -start_number 1097 C:/result/output.%03d.jpg", + "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720" + " -tune stillimage -start_number 1097 C:/result/output.%03d.jpg", # Report from source tiff (996-1096) # 996-1000 = additional 5 head frames # 1001-1095 = source range conformed to 25fps # 1096-1096 = additional 1 tail frames "/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i " - f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996 C:/result/output.%03d.jpg" + f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996" + f" C:/result/output.%03d.jpg" ] assert calls == expected @@ -179,13 +180,13 @@ def test_short_movie_head_gap_handles(): expected = [ # 10 head black frames generated from gap (991-1000) - "/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune " - "stillimage -start_number 991 C:/result/output.%03d.jpg", + "/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720" + " -tune stillimage -start_number 991 C:/result/output.%03d.jpg", # source range + 10 tail frames # duration = 50fr (source) + 10fr (tail handle) = 60 fr = 2.4s - "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4 -start_number 1001 " - "C:/result/output.%03d.jpg" + "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4" + " -start_number 1001 C:/result/output.%03d.jpg" ] assert calls == expected @@ -208,7 +209,8 @@ def test_short_movie_tail_gap_handles(): # 10 head frames + source range # duration = 10fr (head handle) + 66fr (source) = 76fr = 3.16s "/path/to/ffmpeg -ss 1.0416666666666667 -t 3.1666666666666665 -i " - "C:\\data\\qt_no_tc_24fps.mov -start_number 991 C:/result/output.%03d.jpg" + "C:\\data\\qt_no_tc_24fps.mov -start_number 991" + " C:/result/output.%03d.jpg" ] assert calls == expected @@ -234,10 +236,12 @@ def test_multiple_review_clips_no_gap(): expected = [ # 10 head black frames generated from gap (991-1000) - '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720 -tune ' + '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi' + ' -i color=c=black:s=1280x720 -tune ' 'stillimage -start_number 991 C:/result/output.%03d.jpg', - # Alternance 25fps tiff sequence and 24fps exr sequence for 100 frames each + # Alternance 25fps tiff sequence and 24fps exr sequence + # for 100 frames each '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' '-start_number 1001 C:/result/output.%03d.jpg', @@ -315,7 +319,8 @@ def test_multiple_review_clips_with_gap(): expected = [ # Gap on review track (12 frames) - '/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi -i color=c=black:s=1280x720 -tune ' + '/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi' + ' -i color=c=black:s=1280x720 -tune ' 'stillimage -start_number 991 C:/result/output.%03d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' From 3de2755de52567694e4dd1a248cbbfa69499e1b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:37:18 +0100 Subject: [PATCH 305/546] remove unrelated information from docstring --- client/ayon_core/lib/local_settings.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 690781151c..08030ae87e 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -276,12 +276,7 @@ class ASettingRegistry(ABC): @abstractmethod def _delete_item(self, name): # type: (str) -> None - """Delete item from settings. - - Note: - see :meth:`ayon_core.lib.user_settings.ARegistrySettings.delete_item` - - """ + """Delete item from settings.""" pass def __delitem__(self, name): @@ -433,12 +428,7 @@ class IniSettingRegistry(ASettingRegistry): config.write(cfg) def _delete_item(self, name): - """Delete item from default section. - - Note: - See :meth:`~ayon_core.lib.IniSettingsRegistry.delete_item_from_section` - - """ + """Delete item from default section.""" self.delete_item_from_section("MAIN", name) From ef6d7b5a6ce863c0a0231fb8da5ea939b6d29717 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 10:37:27 +0100 Subject: [PATCH 306/546] put noqa at correct place --- client/ayon_core/pipeline/entity_uri.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/entity_uri.py b/client/ayon_core/pipeline/entity_uri.py index 1dee9a1423..1362389ee9 100644 --- a/client/ayon_core/pipeline/entity_uri.py +++ b/client/ayon_core/pipeline/entity_uri.py @@ -18,13 +18,13 @@ def parse_ayon_entity_uri(uri: str) -> Optional[dict]: Example: >>> parse_ayon_entity_uri( - >>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd" # noqa: E501 + >>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd" >>> ) {'project': 'test', 'folderPath': '/char/villain', 'product': 'modelMain', 'version': 1, 'representation': 'usd'} >>> parse_ayon_entity_uri( - >>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr" # noqa: E501 + >>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr" >>> ) {'project': 'project', 'folderPath': '/folder', 'product': 'renderMain', 'version': 3, @@ -34,7 +34,7 @@ def parse_ayon_entity_uri(uri: str) -> Optional[dict]: dict[str, Union[str, int]]: The individual key with their values as found in the ayon entity URI. - """ + """ # noqa: E501 if not (uri.startswith("ayon+entity://") or uri.startswith("ayon://")): return {} From 9cd354efb299c43e27864119ea1779116407ee23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:09:45 +0100 Subject: [PATCH 307/546] added constant to define store key for env variables --- client/ayon_core/pipeline/publish/__init__.py | 2 ++ client/ayon_core/pipeline/publish/constants.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/publish/__init__.py b/client/ayon_core/pipeline/publish/__init__.py index ac71239acf..5363e0b378 100644 --- a/client/ayon_core/pipeline/publish/__init__.py +++ b/client/ayon_core/pipeline/publish/__init__.py @@ -3,6 +3,7 @@ from .constants import ( ValidateContentsOrder, ValidateSceneOrder, ValidateMeshOrder, + FARM_JOB_ENV_DATA_KEY, ) from .publish_plugins import ( @@ -59,6 +60,7 @@ __all__ = ( "ValidateContentsOrder", "ValidateSceneOrder", "ValidateMeshOrder", + "FARM_JOB_ENV_DATA_KEY", "AbstractMetaInstancePlugin", "AbstractMetaContextPlugin", diff --git a/client/ayon_core/pipeline/publish/constants.py b/client/ayon_core/pipeline/publish/constants.py index 38f5ffef3f..f2f4e851a9 100644 --- a/client/ayon_core/pipeline/publish/constants.py +++ b/client/ayon_core/pipeline/publish/constants.py @@ -9,3 +9,5 @@ ValidateMeshOrder = pyblish.api.ValidatorOrder + 0.3 DEFAULT_PUBLISH_TEMPLATE = "default" DEFAULT_HERO_PUBLISH_TEMPLATE = "default" TRANSIENT_DIR_TEMPLATE = "default" + +FARM_JOB_ENV_DATA_KEY: str = "farmJobEnv" From fae4eed3ed97b306b93bd2e98f79bbee335b8604 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:10:59 +0100 Subject: [PATCH 308/546] added new plugin collecting environment variables to context --- .../publish/collect_farm_env_variables.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 client/ayon_core/plugins/publish/collect_farm_env_variables.py diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py new file mode 100644 index 0000000000..935b4d5c9f --- /dev/null +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -0,0 +1,48 @@ +import os + +import pyblish.api + +from ayon_core.pipeline.publish import FARM_JOB_ENV_DATA_KEY + + +class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): + """Collect set of environment variables to submit with deadline jobs""" + order = pyblish.api.CollectorOrder - 0.45 + label = "AYON core Farm Environment Variables" + targets = ["local"] + + ENV_KEYS = [ + # AYON + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", + "AYON_PROJECT_NAME", + "AYON_FOLDER_PATH", + "AYON_TASK_NAME", + "AYON_APP_NAME", + "AYON_WORKDIR", + "AYON_APP_NAME", + "AYON_LOG_NO_COLORS", + "AYON_IN_TESTS", + "IS_TEST", # backwards compatibility + ] + + def process(self, context): + env = context.data.setdefault(FARM_JOB_ENV_DATA_KEY, {}) + for key in [ + # AYON + "AYON_BUNDLE_NAME", + "AYON_DEFAULT_SETTINGS_VARIANT", + "AYON_PROJECT_NAME", + "AYON_FOLDER_PATH", + "AYON_TASK_NAME", + "AYON_WORKDIR", + "AYON_LOG_NO_COLORS", + "AYON_IN_TESTS", + # backwards compatibility + "IS_TEST", + ]: + value = os.getenv(key) + if value: + self.log.debug(f"Setting job env: {key}: {value}") + env[key] = value + From 85be6b2e422b327d6c506018192f3e10a8fee998 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:53:38 +0100 Subject: [PATCH 309/546] remove unnecessary attribute --- .../plugins/publish/collect_farm_env_variables.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 935b4d5c9f..0201973643 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -11,21 +11,6 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): label = "AYON core Farm Environment Variables" targets = ["local"] - ENV_KEYS = [ - # AYON - "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_APP_NAME", - "AYON_WORKDIR", - "AYON_APP_NAME", - "AYON_LOG_NO_COLORS", - "AYON_IN_TESTS", - "IS_TEST", # backwards compatibility - ] - def process(self, context): env = context.data.setdefault(FARM_JOB_ENV_DATA_KEY, {}) for key in [ From 07c246ba74ceb628a05ccd11bfea5e20bf393cfe Mon Sep 17 00:00:00 2001 From: ynbot Date: Mon, 25 Nov 2024 13:57:32 +0000 Subject: [PATCH 310/546] [Automated] Update assign_pr_to_project caller workflow --- .github/workflows/assign_pr_to_project.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/workflows/assign_pr_to_project.yml diff --git a/.github/workflows/assign_pr_to_project.yml b/.github/workflows/assign_pr_to_project.yml new file mode 100644 index 0000000000..86707fc9da --- /dev/null +++ b/.github/workflows/assign_pr_to_project.yml @@ -0,0 +1,15 @@ +name: 🔸Auto assign pr +on: + pull_request: + types: + - opened + +jobs: + auto-assign-pr: + uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@develop + with: + repo: "${{ github.repository }}" + project_id: 16 + pull_request_number: ${{ github.event.pull_request.number }} + secrets: + token: ${{ secrets.YNPUT_BOT_TOKEN }} From 333363f024119022437e5b520b2faa7bcf82e392 Mon Sep 17 00:00:00 2001 From: ynbot Date: Mon, 25 Nov 2024 14:07:54 +0000 Subject: [PATCH 311/546] [Automated] Update validate_pr_labels caller workflow --- .github/workflows/validate_pr_labels.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/validate_pr_labels.yml diff --git a/.github/workflows/validate_pr_labels.yml b/.github/workflows/validate_pr_labels.yml new file mode 100644 index 0000000000..00e5742afe --- /dev/null +++ b/.github/workflows/validate_pr_labels.yml @@ -0,0 +1,18 @@ +name: 🔎 Validate PR Labels +on: + pull_request: + types: + - opened + - edited + - labeled + - unlabeled + +jobs: + validate-type-label: + uses: ynput/ops-repo-automation/.github/workflows/validate_pr_labels.yml@develop + with: + repo: "${{ github.repository }}" + pull_request_number: ${{ github.event.pull_request.number }} + query_prefix: "type: " + secrets: + token: ${{ secrets.YNPUT_BOT_TOKEN }} From 463ad79a062b1fe3e23b3351189ef1200de678fb Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Mon, 25 Nov 2024 10:14:38 -0500 Subject: [PATCH 312/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/stagingdir.py | 46 +++++++++++++------------ 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/stagingdir.py index c7cc95ff55..4395f1a5d5 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/stagingdir.py @@ -1,7 +1,7 @@ from ayon_core.lib import Logger, filter_profiles, StringTemplate from ayon_core.settings import get_project_settings -from ayon_core.pipeline.template_data import get_template_data +from .template_data import get_template_data from .anatomy import Anatomy from .tempdir import get_temp_dir @@ -71,7 +71,7 @@ def get_staging_dir_config( template_name = profile["template_name"] _validate_template_name(project_name, template_name, anatomy) - template = anatomy.templates[STAGING_DIR_TEMPLATES][template_name] + template = anatomy.get_template_item("staging", template_name) if not template: # template should always be found either from anatomy or from profile @@ -93,7 +93,7 @@ def _validate_template_name(project_name, template_name, anatomy): Raises: ValueError - if misconfigured template """ - if template_name not in anatomy.templates[STAGING_DIR_TEMPLATES]: + if template_name not in anatomy.templates["staging"]: raise ValueError( ( 'Anatomy of project "{}" does not have set' @@ -195,23 +195,25 @@ def get_staging_dir_info( log=log, ) - if not staging_dir_config: - if always_return_path: # no config found but force an output - return { - "stagingDir": get_temp_dir( - project_name=project_entity["name"], - anatomy=anatomy, - prefix=prefix, - suffix=suffix, - ), - "stagingDir_persistent": False, - } - else: - return None + if staging_dir_config: + return { + "stagingDir": StringTemplate.format_template( + staging_dir_config["template"]["directory"], + ctx_data + ), + "stagingDir_persistent": staging_dir_config["persistence"], + } - return { - "stagingDir": StringTemplate.format_template( - staging_dir_config["template"]["directory"], ctx_data - ), - "stagingDir_persistent": staging_dir_config["persistence"], - } + # no config found but force an output + if always_return_path: + return { + "stagingDir": get_temp_dir( + project_name=project_entity["name"], + anatomy=anatomy, + prefix=prefix, + suffix=suffix, + ), + "stagingDir_persistent": False, + } + + return None From 0a13574509b517e3d3dd796e7f73ee4d42ce10a4 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 10:20:12 -0500 Subject: [PATCH 313/546] Rename stagingdir to staging_dir. --- client/ayon_core/pipeline/__init__.py | 2 +- client/ayon_core/pipeline/publish/lib.py | 2 +- client/ayon_core/pipeline/{stagingdir.py => staging_dir.py} | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) rename client/ayon_core/pipeline/{stagingdir.py => staging_dir.py} (99%) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index c58e385d79..41bcd0dbd1 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -9,7 +9,7 @@ from .anatomy import Anatomy from .tempdir import get_temp_dir -from .stagingdir import get_staging_dir_info +from .staging_dir import get_staging_dir_info from .create import ( BaseCreator, diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 4c36f473d1..c0dfe8c910 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -642,7 +642,7 @@ def get_custom_staging_dir_info( anatomy=None, log=None, ): - from ayon_core.pipeline.stagingdir import get_staging_dir_config + from ayon_core.pipeline.staging_dir import get_staging_dir_config warnings.warn( ( "Function 'get_custom_staging_dir_info' in" diff --git a/client/ayon_core/pipeline/stagingdir.py b/client/ayon_core/pipeline/staging_dir.py similarity index 99% rename from client/ayon_core/pipeline/stagingdir.py rename to client/ayon_core/pipeline/staging_dir.py index 4395f1a5d5..0e993ecae1 100644 --- a/client/ayon_core/pipeline/stagingdir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -5,8 +5,6 @@ from .template_data import get_template_data from .anatomy import Anatomy from .tempdir import get_temp_dir -STAGING_DIR_TEMPLATES = "staging" - def get_staging_dir_config( project_name, From 2d6911513feab68c4aaf3bba66051bf0babbf196 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 10:23:43 -0500 Subject: [PATCH 314/546] Fix lint. --- client/ayon_core/pipeline/staging_dir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 0e993ecae1..e46426057d 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -203,7 +203,7 @@ def get_staging_dir_info( } # no config found but force an output - if always_return_path: + if always_return_path: return { "stagingDir": get_temp_dir( project_name=project_entity["name"], From 2066fe61a124e0639735cf533ac33894287271cf Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 10:55:04 -0500 Subject: [PATCH 315/546] Fix anatomy template. --- client/ayon_core/pipeline/staging_dir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index e46426057d..86aaf3002f 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -196,7 +196,7 @@ def get_staging_dir_info( if staging_dir_config: return { "stagingDir": StringTemplate.format_template( - staging_dir_config["template"]["directory"], + str(staging_dir_config["template"]["directory"]), ctx_data ), "stagingDir_persistent": staging_dir_config["persistence"], From a60796eb73f631ed14f8c0c4bcd193b05c40a5c3 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 14:27:13 -0500 Subject: [PATCH 316/546] Adjust missing taskEntity. --- client/ayon_core/pipeline/staging_dir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 86aaf3002f..c8e3251e7b 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -147,6 +147,7 @@ def get_staging_dir_info( Optional[Dict[str, Any]]: Staging dir info data """ + task_entity = task_entity or {} log = logger or Logger.get_logger("get_staging_dir_info") if anatomy is None: From 1c7ab66246365903fc8aef14be184bf56cc8731c Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 16:07:17 -0500 Subject: [PATCH 317/546] Fix audio extraction from OTIO timeline. --- .../publish/extract_otio_audio_tracks.py | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 98723beffa..88eb2da059 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -71,20 +71,17 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): name = inst.data["folderPath"] recycling_file = [f for f in created_files if name in f] - - # frameranges - timeline_in_h = inst.data["clipInH"] - timeline_out_h = inst.data["clipOutH"] - fps = inst.data["fps"] - - # create duration - duration = (timeline_out_h - timeline_in_h) + 1 + audio_clip = inst.data["otioClip"] + audio_range = audio_clip.range_in_parent() + duration = audio_range.duration.to_frames() # ffmpeg generate new file only if doesn't exists already if not recycling_file: - # convert to seconds - start_sec = float(timeline_in_h / fps) - duration_sec = float(duration / fps) + parent_track = audio_clip.parent() + parent_track_start = parent_track.range_in_parent().start_time + relative_start_time = audio_range.start_time - parent_track_start + start_sec = relative_start_time.to_seconds() + duration_sec = audio_range.duration.to_seconds() # temp audio file audio_fpath = self.create_temp_file(name) @@ -163,9 +160,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): output = [] # go trough all audio tracks - for otio_track in otio_timeline.tracks: - if "Audio" not in otio_track.kind: - continue + for otio_track in otio_timeline.audio_tracks(): self.log.debug("_" * 50) playhead = 0 for otio_clip in otio_track: @@ -173,19 +168,22 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): if isinstance(otio_clip, otio.schema.Gap): playhead += otio_clip.source_range.duration.value elif isinstance(otio_clip, otio.schema.Clip): - start = otio_clip.source_range.start_time.value - duration = otio_clip.source_range.duration.value - fps = otio_clip.source_range.start_time.rate + media_av_start = otio_clip.available_range().start_time + clip_start = otio_clip.source_range.start_time + fps = clip_start.rate + conformed_av_start = media_av_start.rescaled_to(fps) + start = clip_start - conformed_av_start # ffmpeg ignores embedded tc + duration = otio_clip.source_range.duration media_path = otio_clip.media_reference.target_url input = { "mediaPath": media_path, "delayFrame": playhead, - "startFrame": start, - "durationFrame": duration, + "startFrame": start.to_frames(), + "durationFrame": duration.to_frames(), "delayMilSec": int(float(playhead / fps) * 1000), - "startSec": float(start / fps), - "durationSec": float(duration / fps), - "fps": fps + "startSec": start.to_seconds(), + "durationSec": duration.to_seconds(), + "fps": float(fps) } if input not in output: output.append(input) From 3f8430dceac2132629a510e298a56f165a2998a0 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 16:13:28 -0500 Subject: [PATCH 318/546] Fix lint. --- client/ayon_core/plugins/publish/extract_otio_audio_tracks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 88eb2da059..d80d745111 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -172,7 +172,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): clip_start = otio_clip.source_range.start_time fps = clip_start.rate conformed_av_start = media_av_start.rescaled_to(fps) - start = clip_start - conformed_av_start # ffmpeg ignores embedded tc + # ffmpeg ignores embedded tc + start = clip_start - conformed_av_start duration = otio_clip.source_range.duration media_path = otio_clip.media_reference.target_url input = { From d891a0088fdb90bfbddb5bcc332dd3691596a1e1 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 25 Nov 2024 16:14:53 -0500 Subject: [PATCH 319/546] Fix lint. --- client/ayon_core/plugins/publish/extract_otio_audio_tracks.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index d80d745111..3d22894a75 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -79,7 +79,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): if not recycling_file: parent_track = audio_clip.parent() parent_track_start = parent_track.range_in_parent().start_time - relative_start_time = audio_range.start_time - parent_track_start + relative_start_time = ( + audio_range.start_time - parent_track_start) start_sec = relative_start_time.to_seconds() duration_sec = audio_range.duration.to_seconds() From 0c80fe0ad6d48e854ba0bed5fdeba61e4bcf116f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Nov 2024 23:26:35 +0100 Subject: [PATCH 320/546] The `_representation_conversion` method converts in-place - it does not return anything --- client/ayon_core/pipeline/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index 366c261e08..55c840f3a5 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -387,7 +387,7 @@ def get_representations_delivery_template_data( # convert representation entity. Fixed in 'ayon_api' 1.0.10. if isinstance(template_data, str): con = ayon_api.get_server_api_connection() - repre_entity = con._representation_conversion(repre_entity) + con._representation_conversion(repre_entity) template_data = repre_entity["context"] template_data.update(copy.deepcopy(general_template_data)) From cd5f89afe572637dc111337ac343655b785dc25f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:58:05 +0100 Subject: [PATCH 321/546] remove OpenPype env key --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 0201973643..7b4618527b 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -23,8 +23,6 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): "AYON_WORKDIR", "AYON_LOG_NO_COLORS", "AYON_IN_TESTS", - # backwards compatibility - "IS_TEST", ]: value = os.getenv(key) if value: From c7940b4fd0f175892139a424ceb9922c5325e820 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:00:25 +0100 Subject: [PATCH 322/546] added comment to workdir env --- .../ayon_core/plugins/publish/collect_farm_env_variables.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 7b4618527b..a7d9bce08d 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -14,15 +14,15 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): def process(self, context): env = context.data.setdefault(FARM_JOB_ENV_DATA_KEY, {}) for key in [ - # AYON "AYON_BUNDLE_NAME", "AYON_DEFAULT_SETTINGS_VARIANT", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", - "AYON_WORKDIR", "AYON_LOG_NO_COLORS", "AYON_IN_TESTS", + # NOTE Not sure why workdir is needed? + "AYON_WORKDIR", ]: value = os.getenv(key) if value: From a5842c4fdfc01088ad21bc6a3743145878a64ac8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 Nov 2024 10:22:59 +0100 Subject: [PATCH 323/546] added missing env keys for farm --- .../ayon_core/plugins/publish/collect_farm_env_variables.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index a7d9bce08d..2e28b1b164 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -13,9 +13,14 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): def process(self, context): env = context.data.setdefault(FARM_JOB_ENV_DATA_KEY, {}) + + # Disable colored logs on farm + env["AYON_LOG_NO_COLORS"] = "1" + for key in [ "AYON_BUNDLE_NAME", "AYON_DEFAULT_SETTINGS_VARIANT", + "AYON_USERNAME", "AYON_PROJECT_NAME", "AYON_FOLDER_PATH", "AYON_TASK_NAME", From 73420cd8a0c8c3f60bcad65fba29096c7c3de7df Mon Sep 17 00:00:00 2001 From: ynbot Date: Tue, 26 Nov 2024 11:46:45 +0000 Subject: [PATCH 324/546] [Automated] Update assign_pr_to_project caller workflow --- .github/workflows/assign_pr_to_project.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/assign_pr_to_project.yml b/.github/workflows/assign_pr_to_project.yml index 86707fc9da..4bb3d1742c 100644 --- a/.github/workflows/assign_pr_to_project.yml +++ b/.github/workflows/assign_pr_to_project.yml @@ -6,7 +6,7 @@ on: jobs: auto-assign-pr: - uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@develop + uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main with: repo: "${{ github.repository }}" project_id: 16 From d663d68890fc9c77f4a8e22414c79cbaf5c1e2b5 Mon Sep 17 00:00:00 2001 From: ynbot Date: Tue, 26 Nov 2024 11:52:16 +0000 Subject: [PATCH 325/546] [Automated] Update validate_pr_labels caller workflow --- .github/workflows/validate_pr_labels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validate_pr_labels.yml b/.github/workflows/validate_pr_labels.yml index 00e5742afe..f25e263c98 100644 --- a/.github/workflows/validate_pr_labels.yml +++ b/.github/workflows/validate_pr_labels.yml @@ -9,7 +9,7 @@ on: jobs: validate-type-label: - uses: ynput/ops-repo-automation/.github/workflows/validate_pr_labels.yml@develop + uses: ynput/ops-repo-automation/.github/workflows/validate_pr_labels.yml@main with: repo: "${{ github.repository }}" pull_request_number: ${{ github.event.pull_request.number }} From 47fa5e56027d7ac182b78b64391cea64f61fa032 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Nov 2024 20:21:21 +0800 Subject: [PATCH 326/546] check on the active product id before adding version_items --- client/ayon_core/tools/sceneinventory/view.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 5892e4f983..93c889d037 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -805,7 +805,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_items_by_project[project_name] = version_items_by_product_id active_version_id = active_repre_info.version_id - # active_product_id = active_repre_info.product_id + active_product_id = active_repre_info.product_id versions = set() product_ids = set() @@ -820,21 +820,23 @@ class SceneInventoryView(QtWidgets.QTreeView): ) versions |= { version_item.version - for version_item in version_items_by_product_id.values() + for version_item in + version_items_by_product_id[active_product_id].values() } - for version_item in version_items_by_product_id.values(): - version = version_item.version - _prod_version = version - if _prod_version < 0: - _prod_version = -1 - product_ids_by_version[_prod_version].add( - version_item.product_id - ) - product_ids.add(version_item.product_id) - if version in versions: - continue - versions.add(version) - version_items.append((project_name, version_item)) + for version_item_by_id in version_items_by_product_id.values(): + for version_item in version_item_by_id.values(): + version = version_item.version + _prod_version = version + if _prod_version < 0: + _prod_version = -1 + product_ids_by_version[_prod_version].add( + version_item.product_id + ) + product_ids.add(version_item.product_id) + if version in versions: + continue + versions.add(version) + version_items.append((project_name, version_item)) def version_sorter(_, item): hero_value = 0 From fafcbe8992e5d22efacdfb72b53bca39c16b5126 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Nov 2024 20:24:57 +0800 Subject: [PATCH 327/546] check on the active product id before adding version_items --- client/ayon_core/tools/sceneinventory/view.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 93c889d037..741587c064 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -805,7 +805,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_items_by_project[project_name] = version_items_by_product_id active_version_id = active_repre_info.version_id - active_product_id = active_repre_info.product_id + # active_product_id = active_repre_info.product_id versions = set() product_ids = set() @@ -818,11 +818,6 @@ class SceneInventoryView(QtWidgets.QTreeView): product_ids_by_version_by_project[project_name] = ( product_ids_by_version ) - versions |= { - version_item.version - for version_item in - version_items_by_product_id[active_product_id].values() - } for version_item_by_id in version_items_by_product_id.values(): for version_item in version_item_by_id.values(): version = version_item.version @@ -838,8 +833,9 @@ class SceneInventoryView(QtWidgets.QTreeView): versions.add(version) version_items.append((project_name, version_item)) - def version_sorter(_, item): + def version_sorter(items): hero_value = 0 + item = items[-1] i_version = item.version if i_version < 0: hero_value = 1 From 10e66c4c39fb6505823255e1e0b040e1c4dacb69 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Nov 2024 20:30:28 +0800 Subject: [PATCH 328/546] comsetic fix --- client/ayon_core/tools/sceneinventory/model.py | 9 ++++++--- .../ayon_core/tools/sceneinventory/models/containers.py | 3 ++- client/ayon_core/tools/sceneinventory/view.py | 7 +++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 79af0e5cf5..235b125eab 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -273,7 +273,8 @@ class InventoryModel(QtGui.QStandardItemModel): ) = self._get_status_data(project_name, status_name) repre_name = ( - repre_info.representation_name or "" + repre_info.representation_name or + "" ) container_model_items = [] for container_item in container_items: @@ -281,7 +282,8 @@ class InventoryModel(QtGui.QStandardItemModel): unique_name = repre_name + object_name item = QtGui.QStandardItem() item.setColumnCount(root_item.columnCount()) - item.setData(container_item.namespace, QtCore.Qt.DisplayRole) + item.setData(container_item.namespace, + QtCore.Qt.DisplayRole) item.setData(self.GRAYOUT_COLOR, NAME_COLOR_ROLE) item.setData(self.GRAYOUT_COLOR, VERSION_COLOR_ROLE) item.setData(item_icon, QtCore.Qt.DecorationRole) @@ -290,7 +292,8 @@ class InventoryModel(QtGui.QStandardItemModel): item.setData(version_label, VERSION_LABEL_ROLE) item.setData(container_item.loader_name, LOADER_NAME_ROLE) item.setData(container_item.object_name, OBJECT_NAME_ROLE) - item.setData(container_item.project_name, PROJECT_NAME_ROLE) + item.setData(container_item.project_name, + PROJECT_NAME_ROLE) item.setData(True, IS_CONTAINER_ITEM_ROLE) item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) container_model_items.append(item) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index b1cbb38587..ec1ed39e87 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -362,7 +362,8 @@ class ContainersModel: current_project_name = self._controller.get_current_project_name() for container in containers: try: - item = ContainerItem.from_container_data(current_project_name, container) + item = ContainerItem.from_container_data( + current_project_name, container) repre_id = item.representation_id try: uuid.UUID(repre_id) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 741587c064..ba23e115c0 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -802,7 +802,9 @@ class SceneInventoryView(QtWidgets.QTreeView): ) repre_info_by_project[project_name] = repres_info - version_items_by_project[project_name] = version_items_by_product_id + version_items_by_project[project_name] = ( + version_items_by_product_id + ) active_version_id = active_repre_info.version_id # active_product_id = active_repre_info.product_id @@ -994,7 +996,8 @@ class SceneInventoryView(QtWidgets.QTreeView): def _on_switch_to_versioned(self, item_ids): # Get container items by ID - containers_items_by_id = self._controller.get_container_items_by_id(item_ids) + containers_items_by_id = self._controller.get_container_items_by_id( + item_ids) # Extract project names and their corresponding representation IDs repre_ids_by_project = collections.defaultdict(set) for container_item in containers_items_by_id.values(): From 899b50ec93a5480d63e6c219119eeff5be4e1683 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 26 Nov 2024 09:08:59 -0500 Subject: [PATCH 329/546] Adjust for missing reference. --- .../plugins/publish/extract_otio_audio_tracks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py index 3d22894a75..472694d334 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -166,9 +166,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): playhead = 0 for otio_clip in otio_track: self.log.debug(otio_clip) - if isinstance(otio_clip, otio.schema.Gap): - playhead += otio_clip.source_range.duration.value - elif isinstance(otio_clip, otio.schema.Clip): + if (isinstance(otio_clip, otio.schema.Clip) and + not otio_clip.media_reference.is_missing_reference): media_av_start = otio_clip.available_range().start_time clip_start = otio_clip.source_range.start_time fps = clip_start.rate @@ -190,7 +189,8 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): if input not in output: output.append(input) self.log.debug("__ input: {}".format(input)) - playhead += otio_clip.source_range.duration.value + + playhead += otio_clip.source_range.duration.value return output From 7ccf04ed586b69200d3fba744ef6ab470d86577d Mon Sep 17 00:00:00 2001 From: ynbot Date: Tue, 26 Nov 2024 15:17:38 +0000 Subject: [PATCH 330/546] [Automated] Update assign_pr_to_project caller workflow --- .github/workflows/assign_pr_to_project.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/assign_pr_to_project.yml b/.github/workflows/assign_pr_to_project.yml index 4bb3d1742c..92d2ff2916 100644 --- a/.github/workflows/assign_pr_to_project.yml +++ b/.github/workflows/assign_pr_to_project.yml @@ -1,5 +1,16 @@ name: 🔸Auto assign pr on: + workflow_dispatch: + inputs: + pr_number: + type: number + description: "Run workflow for this PR number" + required: true + project_id: + type: number + description: "Github Project Number" + required: true + default: 16 pull_request: types: - opened @@ -9,7 +20,7 @@ jobs: uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main with: repo: "${{ github.repository }}" - project_id: 16 - pull_request_number: ${{ github.event.pull_request.number }} + project_id: ${{ inputs.project_id || 16 }} + pull_request_number: ${{ github.event.pull_request.number || inputs.pr_number }} secrets: token: ${{ secrets.YNPUT_BOT_TOKEN }} From 5d7aeaf0a707efac9347213c56a8d27cd741c88a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:50:48 +0100 Subject: [PATCH 331/546] better variable name --- client/ayon_core/tools/sceneinventory/view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index ba23e115c0..43c6c8e2d0 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -820,8 +820,8 @@ class SceneInventoryView(QtWidgets.QTreeView): product_ids_by_version_by_project[project_name] = ( product_ids_by_version ) - for version_item_by_id in version_items_by_product_id.values(): - for version_item in version_item_by_id.values(): + for version_items_by_id in version_items_by_product_id.values(): + for version_item in version_items_by_id.values(): version = version_item.version _prod_version = version if _prod_version < 0: From 1776df18e2375e66a0520fddd9d8ef919d6bcdb3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:50:59 +0100 Subject: [PATCH 332/546] don't store project name to version items --- client/ayon_core/tools/sceneinventory/view.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 43c6c8e2d0..a95a51ae37 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -833,11 +833,10 @@ class SceneInventoryView(QtWidgets.QTreeView): if version in versions: continue versions.add(version) - version_items.append((project_name, version_item)) + version_items.append(version_item) - def version_sorter(items): + def version_sorter(item): hero_value = 0 - item = items[-1] i_version = item.version if i_version < 0: hero_value = 1 @@ -855,8 +854,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_options = [] active_version_idx = 0 - for idx, item in enumerate(version_items): - project_name, version_item = item + for idx, version_item in enumerate(version_items): version = version_item.version label = format_version(version) if version_item.version_id == active_version_id: From 5c115ce166b70391048b83597d06635783da1118 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:04:11 +0100 Subject: [PATCH 333/546] switch dialog can work per project --- .../sceneinventory/switch_dialog/dialog.py | 18 ++++++++------ client/ayon_core/tools/sceneinventory/view.py | 24 ++++++++++++++----- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py index 4977ad13c6..a6d88ed44a 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py @@ -46,8 +46,13 @@ class SwitchAssetDialog(QtWidgets.QDialog): switched = QtCore.Signal() - def __init__(self, controller, parent=None, items=None): - super(SwitchAssetDialog, self).__init__(parent) + def __init__(self, controller, project_name, items, parent=None): + super().__init__(parent) + + current_project_name = controller.get_current_project_name() + folder_id = None + if current_project_name == project_name: + folder_id = controller.get_current_folder_id() self.setWindowTitle("Switch selected items ...") @@ -147,11 +152,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): self._init_repre_name = None self._fill_check = False + self._project_name = project_name + self._folder_id = folder_id - self._project_name = controller.get_current_project_name() - self._folder_id = controller.get_current_folder_id() - - self._current_folder_btn.setEnabled(self._folder_id is not None) + self._current_folder_btn.setEnabled(folder_id is not None) self._controller = controller @@ -159,7 +163,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): self._prepare_content_data() def showEvent(self, event): - super(SwitchAssetDialog, self).showEvent(event) + super().showEvent(event) self._show_timer.start() def refresh(self, init_refresh=False): diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index a95a51ae37..918de6f7a4 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -912,14 +912,26 @@ class SceneInventoryView(QtWidgets.QTreeView): def _show_switch_dialog(self, item_ids): """Display Switch dialog""" - containers_by_id = self._controller.get_containers_by_item_ids( + container_items_by_id = self._controller.get_container_items_by_id( item_ids ) - dialog = SwitchAssetDialog( - self._controller, self, list(containers_by_id.values()) - ) - dialog.switched.connect(self.data_changed.emit) - dialog.show() + container_ids_by_project_name = collections.defaultdict(set) + for container_id, container_item in container_items_by_id.values(): + project_name = container_item.project_name + container_ids_by_project_name[project_name].add(container_id) + + for project_name, container_ids in container_ids_by_project_name.items(): + containers_by_id = self._controller.get_containers_by_item_ids( + container_ids + ) + dialog = SwitchAssetDialog( + self._controller, + project_name, + list(containers_by_id.values()), + self + ) + dialog.switched.connect(self.data_changed.emit) + dialog.show() def _show_remove_warning_dialog(self, item_ids): """Prompt a dialog to inform the user the action will remove items""" From 2eb97b972e0adbaef92d3a381b127d52aefa7b0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:12:04 +0100 Subject: [PATCH 334/546] show project name on group instead of items --- client/ayon_core/tools/sceneinventory/model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 235b125eab..885553acaf 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -292,8 +292,6 @@ class InventoryModel(QtGui.QStandardItemModel): item.setData(version_label, VERSION_LABEL_ROLE) item.setData(container_item.loader_name, LOADER_NAME_ROLE) item.setData(container_item.object_name, OBJECT_NAME_ROLE) - item.setData(container_item.project_name, - PROJECT_NAME_ROLE) item.setData(True, IS_CONTAINER_ITEM_ROLE) item.setData(unique_name, ITEM_UNIQUE_NAME_ROLE) container_model_items.append(item) @@ -323,6 +321,7 @@ class InventoryModel(QtGui.QStandardItemModel): group_item.setData(status_short, STATUS_SHORT_ROLE) group_item.setData(status_color, STATUS_COLOR_ROLE) group_item.setData(status_icon, STATUS_ICON_ROLE) + group_item.setData(project_name, PROJECT_NAME_ROLE) group_item.setData( active_site_progress, ACTIVE_SITE_PROGRESS_ROLE From 1ab7a652a3e38fe995105a3658be0ee48cc25a82 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Nov 2024 12:13:55 +0100 Subject: [PATCH 335/546] fix formatting --- client/ayon_core/tools/sceneinventory/view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 918de6f7a4..fd67c43ac7 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -920,7 +920,9 @@ class SceneInventoryView(QtWidgets.QTreeView): project_name = container_item.project_name container_ids_by_project_name[project_name].add(container_id) - for project_name, container_ids in container_ids_by_project_name.items(): + for project_name, container_ids in ( + container_ids_by_project_name.items() + ): containers_by_id = self._controller.get_containers_by_item_ids( container_ids ) From 2842c904d161b05d5e6d8b19473a41a67b6d8646 Mon Sep 17 00:00:00 2001 From: ynbot Date: Fri, 29 Nov 2024 08:17:04 +0000 Subject: [PATCH 336/546] [Automated] Update assign_pr_to_project caller workflow --- .github/workflows/assign_pr_to_project.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/assign_pr_to_project.yml b/.github/workflows/assign_pr_to_project.yml index 92d2ff2916..14e1a02075 100644 --- a/.github/workflows/assign_pr_to_project.yml +++ b/.github/workflows/assign_pr_to_project.yml @@ -17,10 +17,11 @@ on: jobs: auto-assign-pr: + if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main with: repo: "${{ github.repository }}" - project_id: ${{ inputs.project_id || 16 }} - pull_request_number: ${{ github.event.pull_request.number || inputs.pr_number }} + project_id: "${{ inputs.project_id }}" + pull_request_number: "${{ github.event.pull_request.number || inputs.pr_number }}" secrets: token: ${{ secrets.YNPUT_BOT_TOKEN }} From 630f7f6c1e7c53cb69a92a4ed1b42820806e2bb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:59:14 +0100 Subject: [PATCH 337/546] fill values for farm with correct values --- .../publish/collect_farm_env_variables.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 2e28b1b164..cb52e5c32e 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -2,6 +2,7 @@ import os import pyblish.api +from ayon_core.lib import get_ayon_username from ayon_core.pipeline.publish import FARM_JOB_ENV_DATA_KEY @@ -15,16 +16,22 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): env = context.data.setdefault(FARM_JOB_ENV_DATA_KEY, {}) # Disable colored logs on farm - env["AYON_LOG_NO_COLORS"] = "1" + for key, value in ( + ("AYON_LOG_NO_COLORS", "1"), + ("AYON_PROJECT_NAME", context.data["projectName"]), + ("AYON_FOLDER_PATH", context.data.get("folderPath")), + ("AYON_TASK_NAME", context.data.get("task")), + # NOTE we should use 'context.data["user"]' but that has higher + # order. + ("AYON_USERNAME", get_ayon_username()), + ): + if value: + self.log.debug(f"Setting job env: {key}: {value}") + env[key] = value for key in [ "AYON_BUNDLE_NAME", "AYON_DEFAULT_SETTINGS_VARIANT", - "AYON_USERNAME", - "AYON_PROJECT_NAME", - "AYON_FOLDER_PATH", - "AYON_TASK_NAME", - "AYON_LOG_NO_COLORS", "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", From fcbf8ddd91f5ad2e39ba807b24533638d81e985d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 29 Nov 2024 10:25:25 +0000 Subject: [PATCH 338/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index ab8c9424fa..a7373cd291 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.9+dev" +__version__ = "1.0.10" diff --git a/package.py b/package.py index b90db4cde4..b14c38bdd5 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.9+dev" +version = "1.0.10" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index d09fabf8b2..31f00a0fc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.9+dev" +version = "1.0.10" description = "" authors = ["Ynput Team "] readme = "README.md" From 457f234266f0a55486b946a626923abe341df5db Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 29 Nov 2024 10:26:08 +0000 Subject: [PATCH 339/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a7373cd291..b2ece45120 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.10" +__version__ = "1.0.10+dev" diff --git a/package.py b/package.py index b14c38bdd5..58ae5c08d9 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.10" +version = "1.0.10+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 31f00a0fc2..d7cf9fa6ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.10" +version = "1.0.10+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From fdc351f4d05457516191ddf305482c8128296f69 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 29 Nov 2024 14:33:46 +0200 Subject: [PATCH 340/546] fix a typo --- client/ayon_core/tools/attribute_defs/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 46399c5fce..aa500720ed 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -254,7 +254,7 @@ class FilesModel(QtGui.QStandardItemModel): """Make sure that removed items are removed from items mapping. Connected with '_on_insert'. When user drag item and drop it to same - view the item is actually removed and creted again but it happens in + view the item is actually removed and created again but it happens in inner calls of Qt. """ From f40ee8f54793dd8125006935d7ea9d4fc2048fef Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 29 Nov 2024 14:34:24 +0200 Subject: [PATCH 341/546] add missing argument in `context_menu_requested` signal --- client/ayon_core/tools/attribute_defs/files_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index aa500720ed..6199d0c202 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -522,7 +522,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): class ItemWidget(QtWidgets.QWidget): - context_menu_requested = QtCore.Signal(QtCore.QPoint) + context_menu_requested = QtCore.Signal(QtCore.QPoint, bool) def __init__( self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None @@ -589,7 +589,7 @@ class ItemWidget(QtWidgets.QWidget): def _on_actions_clicked(self): pos = self._split_btn.rect().bottomLeft() point = self._split_btn.mapToGlobal(pos) - self.context_menu_requested.emit(point) + self.context_menu_requested.emit(point, False) class InViewButton(IconButton): From 230ae53e4e2d1fbf2ce1c2765b657a6d64365d69 Mon Sep 17 00:00:00 2001 From: ynbot Date: Sat, 30 Nov 2024 14:31:16 +0000 Subject: [PATCH 342/546] [Automated] Update assign_pr_to_project caller workflow --- .github/workflows/assign_pr_to_project.yml | 35 +++++++++++++++++----- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/.github/workflows/assign_pr_to_project.yml b/.github/workflows/assign_pr_to_project.yml index 14e1a02075..e61d281c2a 100644 --- a/.github/workflows/assign_pr_to_project.yml +++ b/.github/workflows/assign_pr_to_project.yml @@ -3,25 +3,46 @@ on: workflow_dispatch: inputs: pr_number: - type: number + type: string description: "Run workflow for this PR number" required: true project_id: - type: number + type: string description: "Github Project Number" required: true - default: 16 + default: "16" pull_request: types: - opened +env: + GH_TOKEN: ${{ github.token }} + jobs: + get-pr-repo: + runs-on: ubuntu-latest + outputs: + pr_repo_name: ${{ steps.get-repo-name.outputs.repo_name || github.event.pull_request.head.repo.full_name }} + + # INFO `github.event.pull_request.head.repo.full_name` is not available on manual triggered (dispatched) runs + steps: + - name: Get PR repo name + if: ${{ github.event_name == 'workflow_dispatch' }} + id: get-repo-name + run: | + repo_name=$(gh pr view ${{ inputs.pr_number }} --json headRepository,headRepositoryOwner --repo ${{ github.repository }} | jq -r '.headRepositoryOwner.login + "/" + .headRepository.name') + echo "repo_name=$repo_name" >> $GITHUB_OUTPUT + auto-assign-pr: - if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} + needs: + - get-pr-repo + if: ${{ needs.get-pr-repo.outputs.pr_repo_name == github.repository }} uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main with: repo: "${{ github.repository }}" - project_id: "${{ inputs.project_id }}" - pull_request_number: "${{ github.event.pull_request.number || inputs.pr_number }}" + project_id: ${{ inputs.project_id != '' && fromJSON(inputs.project_id) || 16 }} + pull_request_number: ${{ github.event.pull_request.number || fromJSON(inputs.pr_number) }} secrets: - token: ${{ secrets.YNPUT_BOT_TOKEN }} + # INFO fallback to default `github.token` is required for PRs from forks + # INFO organization secrets won't be available to forks + token: ${{ secrets.YNPUT_BOT_TOKEN || github.token}} From f5a67f099d291f29789230931bc29f9b9184b4e7 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 2 Dec 2024 14:58:42 -0500 Subject: [PATCH 343/546] Append {version} regex to staging dir. --- .../pipeline/create/creator_plugins.py | 17 +++++++++++--- client/ayon_core/pipeline/publish/lib.py | 8 ++++--- client/ayon_core/pipeline/staging_dir.py | 23 +++++++++++-------- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 93e1f6f5cb..37f3e5b943 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Optional, Dict, Any from abc import ABC, abstractmethod from ayon_core.settings import get_project_settings -from ayon_core.lib import Logger +from ayon_core.lib import Logger, get_version_from_path from ayon_core.pipeline.plugin_discover import ( discover, register_plugin, @@ -860,6 +860,14 @@ class Creator(BaseCreator): else: template_data = {} + # TODO: confirm feature + anatomy_data_settings = self.project_settings["core"]["publish"]["CollectAnatomyInstanceData"] + follow_workfile_version = anatomy_data_settings["follow_workfile_version"] + if follow_workfile_version: + current_workfile = self.create_context.get_current_workfile_path() + workfile_version = get_version_from_path(current_workfile) + template_data = {"version": int(workfile_version)} + staging_dir_info = get_staging_dir_info( create_ctx.get_current_project_entity(), create_ctx.get_current_folder_entity(), @@ -877,12 +885,15 @@ class Creator(BaseCreator): if not staging_dir_info: return None - staging_dir_path = staging_dir_info["stagingDir"] + staging_dir_path = staging_dir_info.dir # path might be already created by get_staging_dir_info os.makedirs(staging_dir_path, exist_ok=True) - instance.transient_data.update(staging_dir_info) + instance.transient_data.update({ + "stagingDir": staging_dir_path, + "stagingDir_persistent": staging_dir_info.persistent, + }) self.log.info(f"Applied staging dir to instance: {staging_dir_path}") diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index c0dfe8c910..b86e439b72 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -710,12 +710,14 @@ def get_instance_staging_dir(instance): always_return_path=True, ) - staging_dir_path = staging_dir_info["stagingDir"] + staging_dir_path = staging_dir_info.dir # path might be already created by get_staging_dir_info os.makedirs(staging_dir_path, exist_ok=True) - - instance.data.update(staging_dir_info) + instance.data.update({ + "stagingDir": staging_dir_path, + "stagingDir_persistent": staging_dir_info.persistent, + }) return staging_dir_path diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index c8e3251e7b..fa216712b2 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -1,3 +1,5 @@ +from collections import namedtuple + from ayon_core.lib import Logger, filter_profiles, StringTemplate from ayon_core.settings import get_project_settings @@ -6,6 +8,9 @@ from .anatomy import Anatomy from .tempdir import get_temp_dir +StagingDir = namedtuple("StagingDir", ["dir", "persistent"]) + + def get_staging_dir_config( project_name, task_type, @@ -144,7 +149,7 @@ def get_staging_dir_info( suffix (Optional[str]): Optional suffix for staging dir name. Returns: - Optional[Dict[str, Any]]: Staging dir info data + Optional[StagingDir]: Staging dir info data """ task_entity = task_entity or {} @@ -195,24 +200,24 @@ def get_staging_dir_info( ) if staging_dir_config: - return { - "stagingDir": StringTemplate.format_template( + return StagingDir( + StringTemplate.format_template( str(staging_dir_config["template"]["directory"]), ctx_data ), - "stagingDir_persistent": staging_dir_config["persistence"], - } + staging_dir_config["persistence"], + ) # no config found but force an output if always_return_path: - return { - "stagingDir": get_temp_dir( + return StagingDir( + get_temp_dir( project_name=project_entity["name"], anatomy=anatomy, prefix=prefix, suffix=suffix, ), - "stagingDir_persistent": False, - } + False, + ) return None From fa014fa93cdd311ea7086d5cc3216deb14c3c14d Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 2 Dec 2024 15:02:08 -0500 Subject: [PATCH 344/546] Fix lint. --- client/ayon_core/pipeline/create/creator_plugins.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 37f3e5b943..87f67a3e80 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -861,8 +861,9 @@ class Creator(BaseCreator): template_data = {} # TODO: confirm feature - anatomy_data_settings = self.project_settings["core"]["publish"]["CollectAnatomyInstanceData"] - follow_workfile_version = anatomy_data_settings["follow_workfile_version"] + publish_settings = self.project_settings["core"]["publish"] + anatomy_settings = publish_settings["CollectAnatomyInstanceData"] + follow_workfile_version = anatomy_settings["follow_workfile_version"] if follow_workfile_version: current_workfile = self.create_context.get_current_workfile_path() workfile_version = get_version_from_path(current_workfile) From 9a0e490233151c116ea0ec2e88ad93fde1d7adda Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 3 Dec 2024 16:52:19 +0800 Subject: [PATCH 345/546] use items() for key, value --- client/ayon_core/tools/sceneinventory/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index fd67c43ac7..bb95e37d4e 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -916,7 +916,7 @@ class SceneInventoryView(QtWidgets.QTreeView): item_ids ) container_ids_by_project_name = collections.defaultdict(set) - for container_id, container_item in container_items_by_id.values(): + for container_id, container_item in container_items_by_id.items(): project_name = container_item.project_name container_ids_by_project_name[project_name].add(container_id) From 648c2c52fe0ac20e1f8bcdb74c19aa6089db0fff Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Tue, 3 Dec 2024 10:50:09 -0500 Subject: [PATCH 346/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../pipeline/create/creator_plugins.py | 11 +++-- client/ayon_core/pipeline/staging_dir.py | 43 ++++++++++--------- client/ayon_core/pipeline/tempdir.py | 3 +- 3 files changed, 31 insertions(+), 26 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 87f67a3e80..780cb71fca 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -862,8 +862,11 @@ class Creator(BaseCreator): # TODO: confirm feature publish_settings = self.project_settings["core"]["publish"] - anatomy_settings = publish_settings["CollectAnatomyInstanceData"] - follow_workfile_version = anatomy_settings["follow_workfile_version"] + follow_workfile_version = ( + publish_settings + ["CollectAnatomyInstanceData"] + ["follow_workfile_version"] + ) if follow_workfile_version: current_workfile = self.create_context.get_current_workfile_path() workfile_version = get_version_from_path(current_workfile) @@ -871,8 +874,8 @@ class Creator(BaseCreator): staging_dir_info = get_staging_dir_info( create_ctx.get_current_project_entity(), - create_ctx.get_current_folder_entity(), - create_ctx.get_current_task_entity(), + create_ctx.get_folder_entity(folder_path), + create_ctx.get_task_entity(folder_path, instance.get("task")), product_type, product_name, create_ctx.host_name, diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index fa216712b2..3c1c7c1ab2 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -8,7 +8,10 @@ from .anatomy import Anatomy from .tempdir import get_temp_dir -StagingDir = namedtuple("StagingDir", ["dir", "persistent"]) +@dataclass +class StagingDir: + directory: str + persistent: bool def get_staging_dir_config( @@ -78,10 +81,9 @@ def get_staging_dir_config( if not template: # template should always be found either from anatomy or from profile - raise ValueError( - "Staging dir profile is misconfigured! " - f"No template was found for profile: {profile}! " - "Check your project settings at: " + raise KeyError( + f"Staging template '{template_name}' was not found." + "Check project anatomy or settings at: " "'ayon+settings://core/tools/publish/custom_staging_dir_profiles'" ) @@ -98,10 +100,8 @@ def _validate_template_name(project_name, template_name, anatomy): """ if template_name not in anatomy.templates["staging"]: raise ValueError( - ( - 'Anatomy of project "{}" does not have set' - ' "{}" template key at Staging Dir section!' - ).format(project_name, template_name) + f'Anatomy of project "{project_name}" does not have set' + f' "{template_name}" template key at Staging Dir category!' ) @@ -131,14 +131,14 @@ def get_staging_dir_info( Arguments: host_name (str): Name of host. project_entity (Dict[str, Any]): Project entity. - folder_entity (Dict[str, Any]): Folder entity. - task_entity (Dict[str, Any]): Task entity. + folder_entity (Optional[Dict[str, Any]]): Folder entity. + task_entity (Optional[Dict[str, Any]]): Task entity. product_type (str): Type of product. product_name (str): Name of product. - anatomy (ayon_core.pipeline.Anatomy): Anatomy object. + anatomy (Optional[Anatomy]): Anatomy object. project_settings (Optional[Dict[str, Any]]): Prepared project settings. - template_data (Optional[Dict[str, Any]]): Data for formatting staging - dir template. + template_data (Optional[Dict[str, Any]]): Additional data for + formatting staging dir template. always_return_path (Optional[bool]): If True, staging dir will be created as tempdir if no staging dir profile is found. Input value False will return None if no staging dir profile is found. @@ -152,7 +152,6 @@ def get_staging_dir_info( Optional[StagingDir]: Staging dir info data """ - task_entity = task_entity or {} log = logger or Logger.get_logger("get_staging_dir_info") if anatomy is None: @@ -185,12 +184,16 @@ def get_staging_dir_info( # add additional template formatting data if template_data: ctx_data.update(template_data) + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] # get staging dir config staging_dir_config = get_staging_dir_config( project_entity["name"], - task_entity.get("taskType"), - task_entity.get("name"), + task_type, + task_name , product_type, product_name, host_name, @@ -200,11 +203,9 @@ def get_staging_dir_info( ) if staging_dir_config: + dir_template = staging_dir_config["template"]["directory"] return StagingDir( - StringTemplate.format_template( - str(staging_dir_config["template"]["directory"]), - ctx_data - ), + dir_template.format_strict(ctx_data), staging_dir_config["persistence"], ) diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index af2ff44a8f..52995d3f6a 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -36,7 +36,8 @@ def get_temp_dir( str: Path to staging dir of instance. """ - prefix = prefix or "ay_tmp_" + if prefix is None: + prefix = "ay_tmp_" suffix = suffix or "" if use_local_temp: From 0672f5c8bb2125ebaafed227f48111e1b1396aeb Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 3 Dec 2024 11:38:29 -0500 Subject: [PATCH 347/546] Address feedback from PR. --- .../pipeline/create/creator_plugins.py | 57 ++++++++++++------- client/ayon_core/pipeline/publish/lib.py | 2 +- client/ayon_core/pipeline/staging_dir.py | 9 +-- 3 files changed, 44 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 780cb71fca..6ccafe1bc7 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -833,17 +833,15 @@ class Creator(BaseCreator): """ return self.pre_create_attr_defs - def apply_staging_dir(self, instance): - """Apply staging dir with persistence to instance's transient data. - - Method is called on instance creation and on instance update. + def get_staging_dir(self, instance): + """Return the staging dir and persistence from instance. Args: instance (CreatedInstance): Instance for which should be staging - dir applied. + dir gathered. Returns: - Optional[str]: Staging dir path or None if not applied. + Optional[namedtuple]: Staging dir path and persistence or None """ create_ctx = self.create_context product_name = instance.get("productName") @@ -852,25 +850,32 @@ class Creator(BaseCreator): # this can only work if product name and folder path are available if not product_name or not folder_path: - return + return None - version = instance.get("version") - if version is not None: - template_data = {"version": version} - else: - template_data = {} - - # TODO: confirm feature publish_settings = self.project_settings["core"]["publish"] follow_workfile_version = ( publish_settings ["CollectAnatomyInstanceData"] ["follow_workfile_version"] ) - if follow_workfile_version: + + # Gather version number provided from the instance. + version = instance.get("version") + + # If follow workfile, gather version from workfile path. + if version is None and follow_workfile_version: current_workfile = self.create_context.get_current_workfile_path() workfile_version = get_version_from_path(current_workfile) - template_data = {"version": int(workfile_version)} + version = int(workfile_version) + + # Fill-up version with next version available. + elif version is None: + versions = self.get_next_versions_for_instances( + [instance] + ) + version, = tuple(versions.values()) + + template_data = {"version": version} staging_dir_info = get_staging_dir_info( create_ctx.get_current_project_entity(), @@ -886,12 +891,26 @@ class Creator(BaseCreator): template_data=template_data, ) - if not staging_dir_info: + return staging_dir_info or None + + def apply_staging_dir(self, instance): + """Apply staging dir with persistence to instance's transient data. + + Method is called on instance creation and on instance update. + + Args: + instance (CreatedInstance): Instance for which should be staging + dir applied. + + Returns: + Optional[str]: Staging dir path or None if not applied. + """ + staging_dir_info = self.get_staging_dir(instance) + if staging_dir_info is None: return None - staging_dir_path = staging_dir_info.dir - # path might be already created by get_staging_dir_info + staging_dir_path = staging_dir_info.directory os.makedirs(staging_dir_path, exist_ok=True) instance.transient_data.update({ diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index b86e439b72..2ba40d7687 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -710,7 +710,7 @@ def get_instance_staging_dir(instance): always_return_path=True, ) - staging_dir_path = staging_dir_info.dir + staging_dir_path = staging_dir_info.directory # path might be already created by get_staging_dir_info os.makedirs(staging_dir_path, exist_ok=True) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 3c1c7c1ab2..0317e55720 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -1,6 +1,6 @@ -from collections import namedtuple +from dataclasses import dataclass -from ayon_core.lib import Logger, filter_profiles, StringTemplate +from ayon_core.lib import Logger, filter_profiles from ayon_core.settings import get_project_settings from .template_data import get_template_data @@ -42,7 +42,7 @@ def get_staging_dir_config( Dict or None: Data with directory template and is_persistent or None Raises: - ValueError - if misconfigured template should be used + KeyError - if misconfigured template should be used """ settings = project_settings or get_project_settings(project_name) @@ -129,12 +129,12 @@ def get_staging_dir_info( If `prefix` or `suffix` is not set, default values will be used. Arguments: - host_name (str): Name of host. project_entity (Dict[str, Any]): Project entity. folder_entity (Optional[Dict[str, Any]]): Folder entity. task_entity (Optional[Dict[str, Any]]): Task entity. product_type (str): Type of product. product_name (str): Name of product. + host_name (str): Name of host. anatomy (Optional[Anatomy]): Anatomy object. project_settings (Optional[Dict[str, Any]]): Prepared project settings. template_data (Optional[Dict[str, Any]]): Additional data for @@ -184,6 +184,7 @@ def get_staging_dir_info( # add additional template formatting data if template_data: ctx_data.update(template_data) + task_name = task_type = None if task_entity: task_name = task_entity["name"] From ed7752a4df7fdfc2c1a27de98ccf890c9a5395ea Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 3 Dec 2024 16:56:53 -0500 Subject: [PATCH 348/546] Fix extract_otio_review --- client/ayon_core/plugins/publish/extract_otio_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index fb9b269258..c8d2086865 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -78,6 +78,7 @@ class ExtractOTIOReview( if otio_review_clips is None: self.log.info(f"Instance `{instance}` has no otioReviewClips") + return # add plugin wide attributes self.representation_files = [] From 9bcc9b40191d0ecdbd435e5a58e714f6dfbd86fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:08:04 +0100 Subject: [PATCH 349/546] fix 'realy' typo to 'really' --- client/ayon_core/lib/path_templates.py | 35 +++++++++++++++++++------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 9b545f2851..bc4ed648b7 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -292,7 +292,7 @@ class TemplatePartResult: # Used values stored by key with all modifirs # - value is already formatted string # Example: {"version:0>3": "001"} - self._realy_used_values = {} + self._really_used_values: Dict[str, Any] = {} # Concatenated string output after formatting self._output = "" # Is this result from optional part @@ -314,7 +314,7 @@ class TemplatePartResult: if other.optional and not other.solved: return self._used_values.update(other.used_values) - self._realy_used_values.update(other.realy_used_values) + self._really_used_values.update(other.really_used_values) else: raise TypeError("Cannot add data from \"{}\" to \"{}\"".format( @@ -359,8 +359,17 @@ class TemplatePartResult: return self._invalid_optional_types @property - def realy_used_values(self): - return self._realy_used_values + def really_used_values(self) -> Dict[str, Any]: + return self._really_used_values + + @property + def realy_used_values(self) -> Dict[str, Any]: + warnings.warn( + "Property 'realy_used_values' is deprecated." + " Use 'really_used_values' instead.", + DeprecationWarning + ) + return self._really_used_values @property def used_values(self): @@ -391,8 +400,16 @@ class TemplatePartResult: return self.split_keys_to_subdicts(new_used_values) - def add_realy_used_value(self, key, value): - self._realy_used_values[key] = value + def add_really_used_value(self, key: str, value: Any): + self._really_used_values[key] = value + + def add_realy_used_value(self, key: str, value: Any): + warnings.warn( + "Method 'add_realy_used_value' is deprecated." + " Use 'add_really_used_value' instead.", + DeprecationWarning + ) + self.add_really_used_value(key, value) def add_used_value(self, key, value): self._used_values[key] = value @@ -519,8 +536,8 @@ class FormattingPart: result(TemplatePartResult): Object where result is stored. """ key = self._template_base - if key in result.realy_used_values: - result.add_output(result.realy_used_values[key]) + if key in result.really_used_values: + result.add_output(result.really_used_values[key]) return result # ensure key is properly formed [({})] properly closed. @@ -625,7 +642,7 @@ class FormattingPart: used_value = value else: used_value = formatted_value - result.add_realy_used_value(self._field_name, used_value) + result.add_really_used_value(self._field_name, used_value) result.add_used_value(used_key, used_value) result.add_output(formatted_value) return result From a80bbfbd5764b33bf51e44b0e92152e6311b58e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:13:33 +0100 Subject: [PATCH 350/546] added basic typehints --- client/ayon_core/lib/path_templates.py | 174 +++++++++++++++---------- 1 file changed, 103 insertions(+), 71 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index bc4ed648b7..3e66344ff4 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -2,8 +2,13 @@ import os import re import copy import numbers -from typing import List +import warnings from string import Formatter +import typing +from typing import List, Dict, Any, Set, Optional + +if typing.TYPE_CHECKING: + from typing import Union SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") @@ -19,9 +24,7 @@ class TemplateUnsolved(Exception): def __init__(self, template, missing_keys, invalid_types): invalid_type_items = [] for _key, _type in invalid_types.items(): - invalid_type_items.append( - "\"{0}\" {1}".format(_key, str(_type)) - ) + invalid_type_items.append(f"\"{_key}\" {str(_type)}") invalid_types_msg = "" if invalid_type_items: @@ -34,20 +37,21 @@ class TemplateUnsolved(Exception): missing_keys_msg = self.missing_keys_msg.format( ", ".join(missing_keys) ) - super(TemplateUnsolved, self).__init__( + super().__init__( self.msg.format(template, missing_keys_msg, invalid_types_msg) ) class StringTemplate: """String that can be formatted.""" - def __init__(self, template): + def __init__(self, template: str): if not isinstance(template, str): - raise TypeError("<{}> argument must be a string, not {}.".format( - self.__class__.__name__, str(type(template)) - )) + raise TypeError( + f"<{self.__class__.__name__}> argument must be a string," + f" not {str(type(template))}." + ) - self._template = template + self._template: str = template parts = [] formatter = Formatter() @@ -78,15 +82,17 @@ class StringTemplate: if substr: new_parts.append(substr) - self._parts = self.find_optional_parts(new_parts) + self._parts: List["Union[str, OptionalPart, FormattingPart]"] = ( + self.find_optional_parts(new_parts) + ) - def __str__(self): + def __str__(self) -> str: return self.template - def __repr__(self): - return "<{}> {}".format(self.__class__.__name__, self.template) + def __repr__(self) -> str: + return f"<{self.__class__.__name__}> {self.template}" - def __contains__(self, other): + def __contains__(self, other: str) -> bool: return other in self.template def replace(self, *args, **kwargs): @@ -94,10 +100,10 @@ class StringTemplate: return self @property - def template(self): + def template(self) -> str: return self._template - def format(self, data): + def format(self, data: Dict[str, Any]) -> "TemplateResult": """ Figure out with whole formatting. Separate advanced keys (*Like '{project[name]}') from string which must @@ -109,6 +115,7 @@ class StringTemplate: Returns: TemplateResult: Filled or partially filled template containing all data needed or missing for filling template. + """ result = TemplatePartResult() for part in self._parts: @@ -136,23 +143,29 @@ class StringTemplate: invalid_types ) - def format_strict(self, *args, **kwargs): - result = self.format(*args, **kwargs) + def format_strict(self, data: Dict[str, Any]) -> "TemplateResult": + result = self.format(data) result.validate() return result @classmethod - def format_template(cls, template, data): + def format_template( + cls, template: str, data: Dict[str, Any] + ) -> "TemplateResult": objected_template = cls(template) return objected_template.format(data) @classmethod - def format_strict_template(cls, template, data): + def format_strict_template( + cls, template: str, data: Dict[str, Any] + ) -> "TemplateResult": objected_template = cls(template) return objected_template.format_strict(data) @staticmethod - def find_optional_parts(parts): + def find_optional_parts( + parts: List["Union[str, FormattingPart]"] + ) -> List["Union[str, OptionalPart, FormattingPart]"]: new_parts = [] tmp_parts = {} counted_symb = -1 @@ -217,11 +230,11 @@ class TemplateResult(str): of number. """ - used_values = None - solved = None - template = None - missing_keys = None - invalid_types = None + used_values: Dict[str, Any] = None + solved: bool = None + template: str = None + missing_keys: List[str] = None + invalid_types: Dict[str, Any] = None def __new__( cls, filled_template, template, solved, @@ -249,7 +262,7 @@ class TemplateResult(str): self.invalid_types ) - def copy(self): + def copy(self) -> "TemplateResult": cls = self.__class__ return cls( str(self), @@ -260,7 +273,7 @@ class TemplateResult(str): self.invalid_types ) - def normalized(self): + def normalized(self) -> "TemplateResult": """Convert to normalized path.""" cls = self.__class__ @@ -276,27 +289,28 @@ class TemplateResult(str): class TemplatePartResult: """Result to store result of template parts.""" - def __init__(self, optional=False): + def __init__(self, optional: bool = False): # Missing keys or invalid value types of required keys - self._missing_keys = set() - self._invalid_types = {} + self._missing_keys: Set[str] = set() + self._invalid_types: Dict[str, Any] = {} # Missing keys or invalid value types of optional keys - self._missing_optional_keys = set() - self._invalid_optional_types = {} + self._missing_optional_keys: Set[str] = set() + self._invalid_optional_types: Dict[str, Any] = {} # Used values stored by key with origin type # - key without any padding or key modifiers # - value from filling data # Example: {"version": 1} - self._used_values = {} + self._used_values: Dict[str, Any] = {} # Used values stored by key with all modifirs # - value is already formatted string # Example: {"version:0>3": "001"} self._really_used_values: Dict[str, Any] = {} # Concatenated string output after formatting - self._output = "" + self._output: str = "" # Is this result from optional part - self._optional = True + # TODO find out why we don't use 'optional' from args + self._optional: bool = True def add_output(self, other): if isinstance(other, str): @@ -322,7 +336,7 @@ class TemplatePartResult: ) @property - def solved(self): + def solved(self) -> bool: if self.optional: if ( len(self.missing_optional_keys) > 0 @@ -335,27 +349,27 @@ class TemplatePartResult: ) @property - def optional(self): + def optional(self) -> bool: return self._optional @property - def output(self): + def output(self) -> str: return self._output @property - def missing_keys(self): + def missing_keys(self) -> Set[str]: return self._missing_keys @property - def missing_optional_keys(self): + def missing_optional_keys(self) -> Set[str]: return self._missing_optional_keys @property - def invalid_types(self): + def invalid_types(self) -> Dict[str, Any]: return self._invalid_types @property - def invalid_optional_types(self): + def invalid_optional_types(self) -> Dict[str, Any]: return self._invalid_optional_types @property @@ -372,11 +386,11 @@ class TemplatePartResult: return self._really_used_values @property - def used_values(self): + def used_values(self) -> Dict[str, Any]: return self._used_values @staticmethod - def split_keys_to_subdicts(values): + def split_keys_to_subdicts(values: Dict[str, Any]) -> Dict[str, Any]: output = {} formatter = Formatter() for key, value in values.items(): @@ -391,7 +405,7 @@ class TemplatePartResult: data[last_key] = value return output - def get_clean_used_values(self): + def get_clean_used_values(self) -> Dict[str, Any]: new_used_values = {} for key, value in self.used_values.items(): if isinstance(value, FormatObject): @@ -411,16 +425,16 @@ class TemplatePartResult: ) self.add_really_used_value(key, value) - def add_used_value(self, key, value): + def add_used_value(self, key: str, value: Any): self._used_values[key] = value - def add_missing_key(self, key): + def add_missing_key(self, key: str): if self._optional: self._missing_optional_keys.add(key) else: self._missing_keys.add(key) - def add_invalid_type(self, key, value): + def add_invalid_type(self, key: str, value: Any): if self._optional: self._invalid_optional_types[key] = type(value) else: @@ -438,10 +452,10 @@ class FormatObject: def __format__(self, *args, **kwargs): return self.value.__format__(*args, **kwargs) - def __str__(self): + def __str__(self) -> str: return str(self.value) - def __repr__(self): + def __repr__(self) -> str: return self.__str__() @@ -451,9 +465,17 @@ class FormattingPart: Containt only single key to format e.g. "{project[name]}". Args: - template(str): String containing the formatting key. + field_name (str): Name of key. + format_spec (str): Format specification. + conversion (Union[str, None]): Conversion type. + """ - def __init__(self, field_name, format_spec, conversion): + def __init__( + self, + field_name: str, + format_spec: str, + conversion: "Union[str, None]", + ): format_spec_v = "" if format_spec: format_spec_v = f":{format_spec}" @@ -461,26 +483,26 @@ class FormattingPart: if conversion: conversion_v = f"!{conversion}" - self._field_name = field_name - self._format_spec = format_spec_v - self._conversion = conversion_v + self._field_name: str = field_name + self._format_spec: str = format_spec_v + self._conversion: str = conversion_v template_base = f"{field_name}{format_spec_v}{conversion_v}" - self._template_base = template_base - self._template = f"{{{template_base}}}" + self._template_base: str = template_base + self._template: str = f"{{{template_base}}}" @property - def template(self): + def template(self) -> str: return self._template - def __repr__(self): + def __repr__(self) -> str: return "".format(self._template) - def __str__(self): + def __str__(self) -> str: return self._template @staticmethod - def validate_value_type(value): + def validate_value_type(value: Any) -> bool: """Check if value can be used for formatting of single key.""" if isinstance(value, (numbers.Number, FormatObject)): return True @@ -491,7 +513,7 @@ class FormattingPart: return False @staticmethod - def validate_key_is_matched(key): + def validate_key_is_matched(key: str) -> bool: """Validate that opening has closing at correct place. Future-proof, only square brackets are currently used in keys. @@ -528,12 +550,15 @@ class FormattingPart: joined_keys = "".join([f"[{key}]" for key in keys]) return f"{template_base}{joined_keys}" - def format(self, data, result): + def format( + self, data: Dict[str, Any], result: TemplatePartResult + ) -> TemplatePartResult: """Format the formattings string. Args: data(dict): Data that should be used for formatting. result(TemplatePartResult): Object where result is stored. + """ key = self._template_base if key in result.really_used_values: @@ -658,20 +683,27 @@ class OptionalPart: 'FormattingPart'. """ - def __init__(self, parts): - self._parts = parts + def __init__( + self, + parts: List["Union[str, OptionalPart, FormattingPart]"] + ): + self._parts: List["Union[str, OptionalPart, FormattingPart]"] = parts @property - def parts(self): + def parts(self) -> List["Union[str, OptionalPart, FormattingPart]"]: return self._parts - def __str__(self): + def __str__(self) -> str: return "<{}>".format("".join([str(p) for p in self._parts])) - def __repr__(self): + def __repr__(self) -> str: return "".format("".join([str(p) for p in self._parts])) - def format(self, data, result): + def format( + self, + data: Dict[str, Any], + result: TemplatePartResult, + ) -> TemplatePartResult: new_result = TemplatePartResult(True) for part in self._parts: if isinstance(part, str): From 04daa9306c3a0f5d70d24fa75fe936a415c6532f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:14:59 +0100 Subject: [PATCH 351/546] remove unused import --- client/ayon_core/lib/path_templates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 3e66344ff4..e3cae78a87 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -5,7 +5,7 @@ import numbers import warnings from string import Formatter import typing -from typing import List, Dict, Any, Set, Optional +from typing import List, Dict, Any, Set if typing.TYPE_CHECKING: from typing import Union From cc23f407afb8d78816dd8c124b0236cf9b8dd2ad Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 4 Dec 2024 11:23:29 -0500 Subject: [PATCH 352/546] Address feedback from PR. --- client/ayon_core/pipeline/staging_dir.py | 11 ++++------- client/ayon_core/pipeline/tempdir.py | 6 ++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 0317e55720..ea22d99389 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -173,13 +173,10 @@ def get_staging_dir_info( ) # add additional data - ctx_data.update({ - "product": { - "type": product_type, - "name": product_name - }, - "root": anatomy.roots - }) + ctx_data["product"] = { + "type": product_type, + "name": product_name + } # add additional template formatting data if template_data: diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index 52995d3f6a..fe057b7fc7 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -65,11 +65,9 @@ def _create_local_staging_dir(prefix, suffix, dirpath=None): str: path to tempdir """ # use pathlib for creating tempdir - staging_dir = Path(tempfile.mkdtemp( + return tempfile.mkdtemp( prefix=prefix, suffix=suffix, dir=dirpath - )) - - return staging_dir.as_posix() + ) def _create_custom_tempdir(project_name, anatomy): From 9f590cd2cec1656c19c407bc77a19dfe728f3fc1 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 4 Dec 2024 16:37:25 -0500 Subject: [PATCH 353/546] Implement review representations in OTIO subset resources. --- .../publish/collect_otio_subset_resources.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index c142036b83..f7b1c9d9b2 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -149,6 +149,7 @@ class CollectOtioSubsetResources( self.log.info( "frame_start-frame_end: {}-{}".format(frame_start, frame_end)) + review_repre = None if is_sequence: # file sequence way @@ -177,6 +178,11 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, collection=collection) + if "review" in instance.data["families"]: + review_repre = self._create_representation( + frame_start, frame_end, collection=collection, + delete=True, review=True) + else: _trim = False dirname, filename = os.path.split(media_ref.target_url) @@ -191,17 +197,26 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, file=filename, trim=_trim) + if "review" in instance.data["families"]: + review_repre = self._create_representation( + frame_start, frame_end, + file=filename, delete=True, review=True) + instance.data["originalDirname"] = self.staging_dir + # add representation to instance data if repre: colorspace = instance.data.get("colorspace") # add colorspace data to representation self.set_representation_colorspace( repre, instance.context, colorspace) - # add representation to instance data instance.data["representations"].append(repre) + # add review representation to instance data + if review_repre: + instance.data["representations"].append(review_repre) + self.log.debug(instance.data) def _create_representation(self, start, end, **kwargs): @@ -221,7 +236,8 @@ class CollectOtioSubsetResources( representation_data = { "frameStart": start, "frameEnd": end, - "stagingDir": self.staging_dir + "stagingDir": self.staging_dir, + "tags": [], } if kwargs.get("collection"): @@ -247,8 +263,10 @@ class CollectOtioSubsetResources( "frameEnd": end, }) - if kwargs.get("trim") is True: - representation_data["tags"] = ["trim"] + for tag_name in ("trim", "delete", "review"): + if kwargs.get(tag_name) is True: + representation_data["tags"].append(tag_name) + return representation_data def get_template_name(self, instance): From 156d3e6a1cd1e9807486ba8c5278f382b3c15058 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 4 Dec 2024 16:45:20 -0500 Subject: [PATCH 354/546] Fix lint. --- .../ayon_core/plugins/publish/collect_otio_subset_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index f7b1c9d9b2..cc1ef3edef 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -215,7 +215,7 @@ class CollectOtioSubsetResources( # add review representation to instance data if review_repre: - instance.data["representations"].append(review_repre) + instance.data["representations"].append(review_repre) self.log.debug(instance.data) From 023e0722f8935b84238292f283a90916e920bedc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:43:18 +0100 Subject: [PATCH 355/546] capture all possible errors that can happen during UUID conversion --- client/ayon_core/tools/loader/control.py | 2 +- client/ayon_core/tools/sceneinventory/models/containers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 2da77337fb..412e6677f0 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -382,7 +382,7 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): try: uuid.UUID(repre_id) repre_ids.add(repre_id) - except ValueError: + except (ValueError, TypeError, AttributeError): pass product_ids = self._products_model.get_product_ids_by_repre_ids( diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 9059485dff..572a96976b 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -230,7 +230,7 @@ class ContainersModel: for repre_id in representation_ids: try: uuid.UUID(repre_id) - except ValueError: + except (ValueError, TypeError, AttributeError): output[repre_id] = RepresentationInfo.new_invalid() continue repre_info = self._repre_info_by_id.get(repre_id) From 2292ecbac11da62427c2007665f587123503cc66 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:45:19 +0100 Subject: [PATCH 356/546] log about invalid representation id --- client/ayon_core/tools/sceneinventory/models/containers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 572a96976b..08b86f6456 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -4,6 +4,7 @@ import collections import ayon_api from ayon_api.graphql import GraphQlQuery +from ayon_core.lib import Logger from ayon_core.host import ILoadHost from ayon_core.tools.common_models.projects import StatusStates @@ -196,6 +197,7 @@ class ContainersModel: self._container_items_by_id = {} self._version_items_by_product_id = {} self._repre_info_by_id = {} + self._log = Logger.get_logger("ContainersModel") def reset(self): self._items_cache = None @@ -368,6 +370,10 @@ class ContainersModel: try: uuid.UUID(repre_id) except (ValueError, TypeError, AttributeError): + self._log.warning( + "Container contains invalid representation id." + f"\n{container}" + ) # Fake not existing representation id so container # is shown in UI but as invalid item.representation_id = invalid_ids_mapping.setdefault( From 373df562543b1fed3c8d00d0b425cd6cbddf61aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:45:35 +0100 Subject: [PATCH 357/546] fix calling of missing method --- client/ayon_core/tools/sceneinventory/models/containers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index 08b86f6456..f25ef2b94c 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -380,10 +380,10 @@ class ContainersModel: repre_id, uuid.uuid4().hex ) - except Exception as e: + except Exception: # skip item if required data are missing - self._controller.log_error( - f"Failed to create item: {e}" + self._log.warning( + f"Failed to create container item", exc_info=True ) continue From b6d3ddc1c8f288e98d68f528334c8c61394f3ecd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:54:47 +0100 Subject: [PATCH 358/546] more safeguard for invalid containers --- client/ayon_core/tools/loader/control.py | 14 +++++++------- .../tools/sceneinventory/models/containers.py | 17 +++++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 412e6677f0..4ce220f282 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -372,14 +372,14 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): repre_ids = set() for container in containers: - repre_id = container.get("representation") - # Ignore invalid representation ids. - # - invalid representation ids may be available if e.g. is - # opened scene from OpenPype whe 'ObjectId' was used instead - # of 'uuid'. - # NOTE: Server call would crash if there is any invalid id. - # That would cause crash we won't get any information. try: + repre_id = container.get("representation") + # Ignore invalid representation ids. + # - invalid representation ids may be available if e.g. is + # opened scene from OpenPype whe 'ObjectId' was used instead + # of 'uuid'. + # NOTE: Server call would crash if there is any invalid id. + # That would cause crash we won't get any information. uuid.UUID(repre_id) repre_ids.add(repre_id) except (ValueError, TypeError, AttributeError): diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index f25ef2b94c..c761121d4d 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -350,12 +350,14 @@ class ContainersModel: return host = self._controller.get_host() - if isinstance(host, ILoadHost): - containers = list(host.get_containers()) - elif hasattr(host, "ls"): - containers = list(host.ls()) - else: - containers = [] + containers = [] + try: + if isinstance(host, ILoadHost): + containers = list(host.get_containers()) + elif hasattr(host, "ls"): + containers = list(host.ls()) + except Exception: + self._log.error("Failed to get containers", exc_info=True) container_items = [] containers_by_id = {} @@ -363,6 +365,9 @@ class ContainersModel: invalid_ids_mapping = {} current_project_name = self._controller.get_current_project_name() for container in containers: + if not container: + continue + try: item = ContainerItem.from_container_data( current_project_name, container) From 3c697b92f57fa597364da39b7e73a03ad963e563 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Dec 2024 14:57:22 +0100 Subject: [PATCH 359/546] remove unnecessary f-string --- client/ayon_core/tools/sceneinventory/models/containers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/sceneinventory/models/containers.py b/client/ayon_core/tools/sceneinventory/models/containers.py index c761121d4d..f841f87c8e 100644 --- a/client/ayon_core/tools/sceneinventory/models/containers.py +++ b/client/ayon_core/tools/sceneinventory/models/containers.py @@ -388,7 +388,7 @@ class ContainersModel: except Exception: # skip item if required data are missing self._log.warning( - f"Failed to create container item", exc_info=True + "Failed to create container item", exc_info=True ) continue From a26e9207d2957402c3de2a23fc0633d85e094675 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Dec 2024 15:10:33 +0100 Subject: [PATCH 360/546] fix long line --- client/ayon_core/tools/loader/control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 4ce220f282..16cf7c31c7 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -376,8 +376,8 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): repre_id = container.get("representation") # Ignore invalid representation ids. # - invalid representation ids may be available if e.g. is - # opened scene from OpenPype whe 'ObjectId' was used instead - # of 'uuid'. + # opened scene from OpenPype whe 'ObjectId' was used + # instead of 'uuid'. # NOTE: Server call would crash if there is any invalid id. # That would cause crash we won't get any information. uuid.UUID(repre_id) From 72862554b4df26b304efb609200b901748c01c4f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Dec 2024 16:13:46 +0100 Subject: [PATCH 361/546] Fix missing slate frame frames_to_render should contain also additional slate frame --- client/ayon_core/pipeline/farm/pyblish_functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 559561c827..c399855044 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -384,6 +384,7 @@ def prepare_representations( frame_end = frames_to_render[-1] if skeleton_data.get("slate"): frame_start -= 1 + frames_to_render.insert(0, frame_start) files = _get_real_files_to_rendered(collection, frames_to_render) From 49c5b875a8a275adb6264474df31a2fc4be3584a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Dec 2024 16:19:58 +0100 Subject: [PATCH 362/546] Refactor removed filtering by frame Remainder has now real way to find frame pattern from single file. Doesn't make sense to filter on single file. --- client/ayon_core/pipeline/farm/pyblish_functions.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 559561c827..147763391b 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -435,13 +435,10 @@ def prepare_representations( " This may cause issues on farm." ).format(staging)) - files = _get_real_files_to_rendered( - [os.path.basename(remainder)], frames_to_render) - rep = { "name": ext, "ext": ext, - "files": files[0], + "files": os.path.basename(remainder), "stagingDir": staging, } From 2a20a9d169b2ed49eada6679166bd61a5db8889e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Dec 2024 16:20:57 +0100 Subject: [PATCH 363/546] Refactor filtering based on frame_to_render Previous implementation was naive and could be dangerous. Updated docstrings. Renamed. --- .../pipeline/farm/pyblish_functions.py | 47 ++++++++++++------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 147763391b..722dc4b5c6 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -385,8 +385,7 @@ def prepare_representations( if skeleton_data.get("slate"): frame_start -= 1 - files = _get_real_files_to_rendered(collection, frames_to_render) - + files = _get_real_files_to_render(collection, frames_to_render) # explicitly disable review by user preview = preview and not do_not_add_review rep = { @@ -492,31 +491,47 @@ def _get_real_frames_to_render(frames): return frames_to_render -def _get_real_files_to_rendered(collection, frames_to_render): - """Use expected files based on real frames_to_render. +def _get_real_files_to_render(collection, frames_to_render): + """Filter files with frames that should be really rendered. + + 'expected_files' are collected from DCC based on timeline setting. This is + being calculated differently in each DCC. Filtering here is on single place + + But artists might explicitly set frames they want to render in Publisher UI + This range would override and filter previously prepared expected files + from DCC. - Artists might explicitly set frames they want to render via Publisher UI. - This uses this value to filter out files Args: - frames_to_render (list): of str '1001' + collection (clique.Collection): absolute paths + frames_to_render (list[int]): of int 1001 + Returns: + (list[str]) + + Example: + -------- + + expectedFiles = [ + "foo_v01.0001.exr", + "foo_v01.0002.exr", + ] + frames_to_render = '0001' + >> + ["foo_v01.0001.exr"] - only explicitly requested frame returned """ - files = [os.path.basename(f) for f in list(collection)] - file_name, extracted_frame = list(collect_frames(files).items())[0] - - if not extracted_frame: - return files - - found_frame_pattern_length = len(extracted_frame) + found_frame_pattern_length = collection.padding normalized_frames_to_render = { str(frame_to_render).zfill(found_frame_pattern_length) for frame_to_render in frames_to_render } + head_name = os.path.basename(collection.head) + + file_names = [os.path.basename(f) for f in collection] return [ file_name - for file_name in files + for file_name in file_names if any( - frame in file_name + f"{head_name}{frame}{collection.tail}" == file_name for frame in normalized_frames_to_render ) ] From b832c850c33cba13bb63cf6b7b58feaba5297510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 5 Dec 2024 16:57:47 +0100 Subject: [PATCH 364/546] Update client/ayon_core/plugins/publish/collect_otio_subset_resources.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_otio_subset_resources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index cc1ef3edef..2d8e91fe09 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -199,8 +199,8 @@ class CollectOtioSubsetResources( if "review" in instance.data["families"]: review_repre = self._create_representation( - frame_start, frame_end, - file=filename, delete=True, review=True) + frame_start, frame_end, + file=filename, delete=True, review=True) instance.data["originalDirname"] = self.staging_dir From 81c71a757fae3ea0120b397017192336becb4a5a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Dec 2024 17:15:40 +0100 Subject: [PATCH 365/546] Prepare normalized expected file names Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/farm/pyblish_functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 722dc4b5c6..a7780ba97c 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -525,7 +525,10 @@ def _get_real_files_to_render(collection, frames_to_render): } head_name = os.path.basename(collection.head) - + normalized_filenames = { + f"{head_name}{frame}{collection.tail}" + for frame in normalized_frames_to_render + } file_names = [os.path.basename(f) for f in collection] return [ file_name From c886be22bdf2494e952c39d4f38959e35c732150 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Dec 2024 17:19:19 +0100 Subject: [PATCH 366/546] Refactor logic to use set --- client/ayon_core/pipeline/farm/pyblish_functions.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index a7780ba97c..1f6e17972a 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -533,10 +533,7 @@ def _get_real_files_to_render(collection, frames_to_render): return [ file_name for file_name in file_names - if any( - f"{head_name}{frame}{collection.tail}" == file_name - for frame in normalized_frames_to_render - ) + if file_name in normalized_filenames ] From 06f6b519f0d45aa5e9dfadf3f184106d98166eb7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Dec 2024 12:42:07 +0100 Subject: [PATCH 367/546] Refactor logic to be even simpler --- .../pipeline/farm/pyblish_functions.py | 25 ++++++------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 1f6e17972a..902aa41af4 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -518,23 +518,14 @@ def _get_real_files_to_render(collection, frames_to_render): >> ["foo_v01.0001.exr"] - only explicitly requested frame returned """ - found_frame_pattern_length = collection.padding - normalized_frames_to_render = { - str(frame_to_render).zfill(found_frame_pattern_length) - for frame_to_render in frames_to_render - } - - head_name = os.path.basename(collection.head) - normalized_filenames = { - f"{head_name}{frame}{collection.tail}" - for frame in normalized_frames_to_render - } - file_names = [os.path.basename(f) for f in collection] - return [ - file_name - for file_name in file_names - if file_name in normalized_filenames - ] + included_frames = set(collection.indexes).intersection(frames_to_render) + real_collection = clique.Collection( + collection.head, + collection.tail, + collection.padding, + indexes=included_frames + ) + return list(real_collection) def create_instances_for_aov(instance, skeleton, aov_filter, From 6836a7f79b546e7dc500d2c8ba911469e3035012 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Dec 2024 13:51:09 +0100 Subject: [PATCH 368/546] Fix docstring --- client/ayon_core/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 902aa41af4..425b616adc 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -514,7 +514,7 @@ def _get_real_files_to_render(collection, frames_to_render): "foo_v01.0001.exr", "foo_v01.0002.exr", ] - frames_to_render = '0001' + frames_to_render = 1 >> ["foo_v01.0001.exr"] - only explicitly requested frame returned """ From d65865563f7aa34ede47b17e2dfc671a03f84255 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Dec 2024 14:53:17 +0100 Subject: [PATCH 369/546] Fix expected files must be only file names --- client/ayon_core/pipeline/farm/pyblish_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 425b616adc..71068cb093 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -525,7 +525,8 @@ def _get_real_files_to_render(collection, frames_to_render): collection.padding, indexes=included_frames ) - return list(real_collection) + real_full_paths = list(real_collection) + return [os.path.basename(file_url) for file_url in real_full_paths] def create_instances_for_aov(instance, skeleton, aov_filter, From 4bf03f0a51ed5b73034b537f55d6c853fb572b3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 9 Dec 2024 15:39:08 +0100 Subject: [PATCH 370/546] Remove unused import 'collect_frames' from pyblish_functions --- client/ayon_core/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 616e25596c..e48d99602e 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -7,7 +7,7 @@ from copy import deepcopy import attr import ayon_api import clique -from ayon_core.lib import Logger, collect_frames +from ayon_core.lib import Logger from ayon_core.pipeline import ( get_current_project_name, get_representation_path, From 55eb8e497fc30f3de22229fea904b223136cf33f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 9 Dec 2024 15:13:06 +0000 Subject: [PATCH 371/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index b2ece45120..0d3b533f57 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.10+dev" +__version__ = "1.0.11" diff --git a/package.py b/package.py index 58ae5c08d9..464fbb007b 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.10+dev" +version = "1.0.11" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index d7cf9fa6ed..ab452816ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.10+dev" +version = "1.0.11" description = "" authors = ["Ynput Team "] readme = "README.md" From a84a3cad33be7e6fc29d6b5539f1e33f556374e4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 9 Dec 2024 15:13:44 +0000 Subject: [PATCH 372/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 0d3b533f57..a4ae75914c 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.11" +__version__ = "1.0.11+dev" diff --git a/package.py b/package.py index 464fbb007b..b8d88fc2ad 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.11" +version = "1.0.11+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index ab452816ad..bdfaf797e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.11" +version = "1.0.11+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 49d15156799dc5f8338ad51cea7689a2174d1a3d Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 9 Dec 2024 18:49:56 -0500 Subject: [PATCH 373/546] AY-7222 Fix otio_review no handles and tempdir for Resolve --- client/ayon_core/pipeline/tempdir.py | 13 +++++++++++++ .../plugins/publish/extract_otio_review.py | 7 ++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index fe057b7fc7..7fb539bf0b 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -5,6 +5,7 @@ Temporary folder operations import os import tempfile from pathlib import Path +import warnings from ayon_core.lib import StringTemplate from ayon_core.pipeline import Anatomy @@ -70,6 +71,18 @@ def _create_local_staging_dir(prefix, suffix, dirpath=None): ) +def create_custom_tempdir(project_name, anatomy): + """ Deprecated 09/12/2024, here for backward-compatibility with Resolve. + """ + warnings.warn( + "Used deprecated 'create_custom_tempdir' " + "use 'ayon_core.pipeline.tempdir.get_temp_dir' instead.", + DeprecationWarning, + ) + + return _create_custom_tempdir(project_name, anatomy) + + def _create_custom_tempdir(project_name, anatomy): """ Create custom tempdir diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index c8d2086865..712ae7a886 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -71,15 +71,16 @@ class ExtractOTIOReview( # TODO: convert resulting image sequence to mp4 # get otio clip and other time info from instance clip - # TODO: what if handles are different in `versionData`? - handle_start = instance.data["handleStart"] - handle_end = instance.data["handleEnd"] otio_review_clips = instance.data.get("otioReviewClips") if otio_review_clips is None: self.log.info(f"Instance `{instance}` has no otioReviewClips") return + # TODO: what if handles are different in `versionData`? + handle_start = instance.data["handleStart"] + handle_end = instance.data["handleEnd"] + # add plugin wide attributes self.representation_files = [] self.used_frames = [] From 55dacd5cec1eb58681bce5bf6d784c6b35ddc401 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:08:37 +0100 Subject: [PATCH 374/546] use 'taskType' instead of 'type' --- .../pipeline/workfile/path_resolving.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index 47d6f4ddfa..dee27ae4db 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -34,15 +34,23 @@ def get_workfile_template_key_from_context( host_name (str): Host name. project_settings (Dict[str, Any]): Project settings for passed 'project_name'. Not required at all but makes function faster. - """ + Returns: + str: Workfile template name. + + """ folder_entity = ayon_api.get_folder_by_path( - project_name, folder_path, fields={"id"} + project_name, + folder_path, + fields={"id"}, ) task_entity = ayon_api.get_task_by_name( - project_name, folder_entity["id"], task_name + project_name, + folder_entity["id"], + task_name, + fields={"taskType"}, ) - task_type = task_entity.get("type") + task_type = task_entity.get("taskType") return get_workfile_template_key( project_name, task_type, host_name, project_settings From 55a4c42c8377ab67777062cf046191b2e83c91ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 14:08:45 +0100 Subject: [PATCH 375/546] added typehints --- .../ayon_core/pipeline/workfile/path_resolving.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index dee27ae4db..61c6e5b876 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -2,6 +2,7 @@ import os import re import copy import platform +from typing import Optional, Dict, Any import ayon_api @@ -16,12 +17,12 @@ from ayon_core.pipeline.template_data import get_template_data def get_workfile_template_key_from_context( - project_name, - folder_path, - task_name, - host_name, - project_settings=None -): + project_name: str, + folder_path: str, + task_name: str, + host_name: str, + project_settings: Optional[Dict[str, Any]] = None, +) -> str: """Helper function to get template key for workfile template. Do the same as `get_workfile_template_key` but returns value for "session From c1904dff39ca2923855dc19999efddb15e41ff6c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 10 Dec 2024 14:31:45 +0100 Subject: [PATCH 376/546] Make sure to operate on copy of data and leave workfile instance data unaffected --- client/ayon_core/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 2ba40d7687..ecdcc0f0c1 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -764,7 +764,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True): return # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") + template_data = copy.deepcopy(workfile_instance.data["anatomyData"]) rep = workfile_instance.data["representations"][0] template_data["representation"] = rep.get("name") template_data["ext"] = rep.get("ext") From 6d7415360e1b3177affd5b734a7e0b332015efc7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:18:08 +0100 Subject: [PATCH 377/546] fix item menu request --- client/ayon_core/tools/attribute_defs/files_widget.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 6199d0c202..42e805d72e 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -522,7 +522,7 @@ class FilesProxyModel(QtCore.QSortFilterProxyModel): class ItemWidget(QtWidgets.QWidget): - context_menu_requested = QtCore.Signal(QtCore.QPoint, bool) + context_menu_requested = QtCore.Signal(QtCore.QPoint) def __init__( self, item_id, label, pixmap_icon, is_sequence, multivalue, parent=None @@ -841,7 +841,7 @@ class FilesWidget(QtWidgets.QFrame): self._multivalue ) widget.context_menu_requested.connect( - self._on_context_menu_requested + self._on_item_context_menu_request ) self._files_view.setIndexWidget(index, widget) self._files_proxy_model.setData( @@ -923,6 +923,9 @@ class FilesWidget(QtWidgets.QFrame): if menu.actions(): menu.popup(pos) + def _on_item_context_menu_request(self, pos): + self._on_context_menu_requested(pos, True) + def dragEnterEvent(self, event): if self._multivalue: return From 38b6aeadbac8978f9416c41a3a8d1be9f9d02b42 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:18:36 +0100 Subject: [PATCH 378/546] don't pass boolean to signal --- client/ayon_core/tools/attribute_defs/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 42e805d72e..652a33e29a 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -589,7 +589,7 @@ class ItemWidget(QtWidgets.QWidget): def _on_actions_clicked(self): pos = self._split_btn.rect().bottomLeft() point = self._split_btn.mapToGlobal(pos) - self.context_menu_requested.emit(point, False) + self.context_menu_requested.emit(point) class InViewButton(IconButton): From 9ea0c79f42da8c2b80ef87b13ee2d1e579ca7a7d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:18:48 +0100 Subject: [PATCH 379/546] use unused variable --- client/ayon_core/tools/attribute_defs/files_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/files_widget.py b/client/ayon_core/tools/attribute_defs/files_widget.py index 652a33e29a..8a40b3ff38 100644 --- a/client/ayon_core/tools/attribute_defs/files_widget.py +++ b/client/ayon_core/tools/attribute_defs/files_widget.py @@ -859,7 +859,7 @@ class FilesWidget(QtWidgets.QFrame): for row in range(self._files_proxy_model.rowCount()): index = self._files_proxy_model.index(row, 0) item_id = index.data(ITEM_ID_ROLE) - available_item_ids.add(index.data(ITEM_ID_ROLE)) + available_item_ids.add(item_id) widget_ids = set(self._widgets_by_id.keys()) for item_id in available_item_ids: From c40062878759558d61e1c6f1d4b5345136f254f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:41:01 +0100 Subject: [PATCH 380/546] added launcher and browser actions to tray --- client/ayon_core/tools/tray/ui/tray.py | 45 ++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index f6a8add861..e61f903c80 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -23,6 +23,7 @@ from ayon_core.addon import ( ITrayAction, ITrayService, ) +from ayon_core.pipeline import install_ayon_plugins from ayon_core.tools.utils import ( WrappedCallbackItem, get_ayon_qt_app, @@ -32,6 +33,8 @@ from ayon_core.tools.tray.lib import ( remove_tray_server_url, TrayIsRunningError, ) +from ayon_core.tools.launcher.ui import LauncherWindow +from ayon_core.tools.loader.ui import LoaderWindow from .addons_manager import TrayAddonsManager from .host_console_listener import HostListener @@ -82,6 +85,9 @@ class TrayManager: self._outdated_dialog = None + self._launcher_window = None + self._browser_window = None + self._update_check_timer = update_check_timer self._update_check_interval = update_check_interval self._main_thread_timer = main_thread_timer @@ -109,12 +115,15 @@ class TrayManager: @property def doubleclick_callback(self): """Double-click callback for Tray icon.""" - return self._addons_manager.get_doubleclick_callback() + callback = self._addons_manager.get_doubleclick_callback() + if callback is None: + callback = self._show_launcher_window + return callback def execute_doubleclick(self): """Execute double click callback in main thread.""" callback = self.doubleclick_callback - if callback: + if callback is not None: self.execute_in_main_thread(callback) def show_tray_message(self, title, message, icon=None, msecs=None): @@ -144,8 +153,22 @@ class TrayManager: return tray_menu = self.tray_widget.menu + self._addons_manager.initialize(tray_menu) + # Add default actions under addon actions + launcher_action = QtWidgets.QAction( + "Launcher", tray_menu + ) + launcher_action.triggered.connect(self._show_launcher_window) + tray_menu.addAction(launcher_action) + + browser_action = QtWidgets.QAction( + "Browser", tray_menu + ) + browser_action.triggered.connect(self._show_browser_window) + tray_menu.addAction(browser_action) + self._addons_manager.add_route( "GET", "/tray", self._web_get_tray_info ) @@ -522,6 +545,24 @@ class TrayManager: self._info_widget.raise_() self._info_widget.activateWindow() + def _show_launcher_window(self): + if self._launcher_window is None: + self._launcher_window = LauncherWindow() + + self._launcher_window.show() + self._launcher_window.raise_() + self._launcher_window.activateWindow() + + def _show_browser_window(self): + if self._browser_window is None: + self._browser_window = LoaderWindow() + self._browser_window.setWindowTitle("AYON Browser") + install_ayon_plugins() + + self._browser_window.show() + self._browser_window.raise_() + self._browser_window.activateWindow() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From 167cea29b5d8bba2c5af44cc22346cdabfbf7eba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:41:11 +0100 Subject: [PATCH 381/546] remove action addons --- client/ayon_core/modules/launcher_action.py | 60 ------------------ client/ayon_core/modules/loader_action.py | 68 --------------------- 2 files changed, 128 deletions(-) delete mode 100644 client/ayon_core/modules/launcher_action.py delete mode 100644 client/ayon_core/modules/loader_action.py diff --git a/client/ayon_core/modules/launcher_action.py b/client/ayon_core/modules/launcher_action.py deleted file mode 100644 index 344b0bc389..0000000000 --- a/client/ayon_core/modules/launcher_action.py +++ /dev/null @@ -1,60 +0,0 @@ -import os - -from ayon_core import AYON_CORE_ROOT -from ayon_core.addon import AYONAddon, ITrayAction - - -class LauncherAction(AYONAddon, ITrayAction): - label = "Launcher" - name = "launcher_tool" - version = "1.0.0" - - def initialize(self, settings): - - # Tray attributes - self._window = None - - def tray_init(self): - self._create_window() - - self.add_doubleclick_callback(self._show_launcher) - - def tray_start(self): - return - - def connect_with_addons(self, enabled_modules): - # Register actions - if not self.tray_initialized: - return - - from ayon_core.pipeline.actions import register_launcher_action_path - - actions_dir = os.path.join(AYON_CORE_ROOT, "plugins", "actions") - if os.path.exists(actions_dir): - register_launcher_action_path(actions_dir) - - actions_paths = self.manager.collect_plugin_paths()["actions"] - for path in actions_paths: - if path and os.path.exists(path): - register_launcher_action_path(path) - - def on_action_trigger(self): - """Implementation for ITrayAction interface. - - Show launcher tool on action trigger. - """ - - self._show_launcher() - - def _create_window(self): - if self._window: - return - from ayon_core.tools.launcher.ui import LauncherWindow - self._window = LauncherWindow() - - def _show_launcher(self): - if self._window is None: - return - self._window.show() - self._window.raise_() - self._window.activateWindow() diff --git a/client/ayon_core/modules/loader_action.py b/client/ayon_core/modules/loader_action.py deleted file mode 100644 index a58d7fd456..0000000000 --- a/client/ayon_core/modules/loader_action.py +++ /dev/null @@ -1,68 +0,0 @@ -from ayon_core.addon import AYONAddon, ITrayAddon - - -class LoaderAddon(AYONAddon, ITrayAddon): - name = "loader_tool" - version = "1.0.0" - - def initialize(self, settings): - # Tray attributes - self._loader_imported = None - self._loader_window = None - - def tray_init(self): - # Add library tool - self._loader_imported = False - try: - from ayon_core.tools.loader.ui import LoaderWindow # noqa F401 - - self._loader_imported = True - except Exception: - self.log.warning( - "Couldn't load Loader tool for tray.", - exc_info=True - ) - - # Definition of Tray menu - def tray_menu(self, tray_menu): - if not self._loader_imported: - return - - from qtpy import QtWidgets - # Actions - action_loader = QtWidgets.QAction( - "Loader", tray_menu - ) - - action_loader.triggered.connect(self.show_loader) - - tray_menu.addAction(action_loader) - - def tray_start(self, *_a, **_kw): - return - - def tray_exit(self, *_a, **_kw): - return - - def show_loader(self): - if self._loader_window is None: - from ayon_core.pipeline import install_ayon_plugins - - self._init_loader() - - install_ayon_plugins() - - self._loader_window.show() - - # Raise and activate the window - # for MacOS - self._loader_window.raise_() - # for Windows - self._loader_window.activateWindow() - - def _init_loader(self): - from ayon_core.tools.loader.ui import LoaderWindow - - libraryloader = LoaderWindow() - - self._loader_window = libraryloader From 77efd56157470058561fdf7b3fffdd7d29595b51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:10:35 +0100 Subject: [PATCH 382/546] created tool with basic separation of some logic to controller --- .../tools/console_interpreter/__init__.py | 8 + .../tools/console_interpreter/abstract.py | 33 ++ .../tools/console_interpreter/control.py | 63 ++++ .../tools/console_interpreter/ui/__init__.py | 8 + .../tools/console_interpreter/ui/utils.py | 42 +++ .../tools/console_interpreter/ui/widgets.py | 251 ++++++++++++++ .../tools/console_interpreter/ui/window.py | 324 ++++++++++++++++++ 7 files changed, 729 insertions(+) create mode 100644 client/ayon_core/tools/console_interpreter/__init__.py create mode 100644 client/ayon_core/tools/console_interpreter/abstract.py create mode 100644 client/ayon_core/tools/console_interpreter/control.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/__init__.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/utils.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/widgets.py create mode 100644 client/ayon_core/tools/console_interpreter/ui/window.py diff --git a/client/ayon_core/tools/console_interpreter/__init__.py b/client/ayon_core/tools/console_interpreter/__init__.py new file mode 100644 index 0000000000..0333fe80a0 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/__init__.py @@ -0,0 +1,8 @@ +from .abstract import AbstractInterpreterController +from .control import InterpreterController + + +__all__ = ( + "AbstractInterpreterController", + "InterpreterController", +) diff --git a/client/ayon_core/tools/console_interpreter/abstract.py b/client/ayon_core/tools/console_interpreter/abstract.py new file mode 100644 index 0000000000..a945e6e498 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/abstract.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from typing import List, Dict, Optional + + +@dataclass +class TabItem: + name: str + code: str + + +@dataclass +class InterpreterConfig: + width: Optional[int] + height: Optional[int] + splitter_sizes: List[int] = field(default_factory=list) + tabs: List[TabItem] = field(default_factory=list) + + +class AbstractInterpreterController(ABC): + @abstractmethod + def get_config(self) -> InterpreterConfig: + pass + + @abstractmethod + def save_config( + self, + width: int, + height: int, + splitter_sizes: List[int], + tabs: List[Dict[str, str]], + ): + pass diff --git a/client/ayon_core/tools/console_interpreter/control.py b/client/ayon_core/tools/console_interpreter/control.py new file mode 100644 index 0000000000..b931b6252c --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/control.py @@ -0,0 +1,63 @@ +from typing import List, Dict + +from ayon_core.lib import JSONSettingRegistry +from ayon_core.lib.local_settings import get_launcher_local_dir + +from .abstract import ( + AbstractInterpreterController, + TabItem, + InterpreterConfig, +) + + +class InterpreterController(AbstractInterpreterController): + def __init__(self): + self._registry = JSONSettingRegistry( + "python_interpreter_tool", + get_launcher_local_dir(), + ) + + def get_config(self): + width = None + height = None + splitter_sizes = [] + tabs = [] + try: + width = self._registry.get_item("width") + height = self._registry.get_item("height") + + except (ValueError, KeyError): + pass + + try: + splitter_sizes = self._registry.get_item("splitter_sizes") + except (ValueError, KeyError): + pass + + try: + tab_defs = self._registry.get_item("tabs") or [] + for tab_def in tab_defs: + tab_name = tab_def.get("name") + if not tab_name: + continue + code = tab_def.get("code") or "" + tabs.append(TabItem(tab_name, code)) + + except (ValueError, KeyError): + pass + + return InterpreterConfig( + width, height, splitter_sizes, tabs + ) + + def save_config( + self, + width: int, + height: int, + splitter_sizes: List[int], + tabs: List[Dict[str, str]], + ): + self._registry.set_item("width", width) + self._registry.set_item("height", height) + self._registry.set_item("splitter_sizes", splitter_sizes) + self._registry.set_item("tabs", tabs) diff --git a/client/ayon_core/tools/console_interpreter/ui/__init__.py b/client/ayon_core/tools/console_interpreter/ui/__init__.py new file mode 100644 index 0000000000..05b166892c --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/__init__.py @@ -0,0 +1,8 @@ +from .window import ( + ConsoleInterpreterWindow +) + + +__all__ = ( + "ConsoleInterpreterWindow", +) diff --git a/client/ayon_core/tools/console_interpreter/ui/utils.py b/client/ayon_core/tools/console_interpreter/ui/utils.py new file mode 100644 index 0000000000..427483215d --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/utils.py @@ -0,0 +1,42 @@ +import os +import sys +import collections + + +class StdOEWrap: + def __init__(self): + self._origin_stdout_write = None + self._origin_stderr_write = None + self._listening = False + self.lines = collections.deque() + + if not sys.stdout: + sys.stdout = open(os.devnull, "w") + + if not sys.stderr: + sys.stderr = open(os.devnull, "w") + + if self._origin_stdout_write is None: + self._origin_stdout_write = sys.stdout.write + + if self._origin_stderr_write is None: + self._origin_stderr_write = sys.stderr.write + + self._listening = True + sys.stdout.write = self._stdout_listener + sys.stderr.write = self._stderr_listener + + def stop_listen(self): + self._listening = False + + def _stdout_listener(self, text): + if self._listening: + self.lines.append(text) + if self._origin_stdout_write is not None: + self._origin_stdout_write(text) + + def _stderr_listener(self, text): + if self._listening: + self.lines.append(text) + if self._origin_stderr_write is not None: + self._origin_stderr_write(text) diff --git a/client/ayon_core/tools/console_interpreter/ui/widgets.py b/client/ayon_core/tools/console_interpreter/ui/widgets.py new file mode 100644 index 0000000000..2b9361666e --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/widgets.py @@ -0,0 +1,251 @@ +from code import InteractiveInterpreter + +from qtpy import QtCore, QtWidgets, QtGui + + +class PythonCodeEditor(QtWidgets.QPlainTextEdit): + execute_requested = QtCore.Signal() + + def __init__(self, parent): + super().__init__(parent) + + self.setObjectName("PythonCodeEditor") + + self._indent = 4 + + def _tab_shift_right(self): + cursor = self.textCursor() + selected_text = cursor.selectedText() + if not selected_text: + cursor.insertText(" " * self._indent) + return + + sel_start = cursor.selectionStart() + sel_end = cursor.selectionEnd() + cursor.setPosition(sel_end) + end_line = cursor.blockNumber() + cursor.setPosition(sel_start) + while True: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + text = cursor.block().text() + spaces = len(text) - len(text.lstrip(" ")) + new_spaces = spaces % self._indent + if not new_spaces: + new_spaces = self._indent + + cursor.insertText(" " * new_spaces) + if cursor.blockNumber() == end_line: + break + + cursor.movePosition(QtGui.QTextCursor.NextBlock) + + def _tab_shift_left(self): + tmp_cursor = self.textCursor() + sel_start = tmp_cursor.selectionStart() + sel_end = tmp_cursor.selectionEnd() + + cursor = QtGui.QTextCursor(self.document()) + cursor.setPosition(sel_end) + end_line = cursor.blockNumber() + cursor.setPosition(sel_start) + while True: + cursor.movePosition(QtGui.QTextCursor.StartOfLine) + text = cursor.block().text() + spaces = len(text) - len(text.lstrip(" ")) + if spaces: + spaces_to_remove = (spaces % self._indent) or self._indent + if spaces_to_remove > spaces: + spaces_to_remove = spaces + + cursor.setPosition( + cursor.position() + spaces_to_remove, + QtGui.QTextCursor.KeepAnchor + ) + cursor.removeSelectedText() + + if cursor.blockNumber() == end_line: + break + + cursor.movePosition(QtGui.QTextCursor.NextBlock) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Backtab: + self._tab_shift_left() + event.accept() + return + + if event.key() == QtCore.Qt.Key_Tab: + if event.modifiers() == QtCore.Qt.NoModifier: + self._tab_shift_right() + event.accept() + return + + if ( + event.key() == QtCore.Qt.Key_Return + and event.modifiers() == QtCore.Qt.ControlModifier + ): + self.execute_requested.emit() + event.accept() + return + + super().keyPressEvent(event) + + +class PythonTabWidget(QtWidgets.QWidget): + add_tab_requested = QtCore.Signal() + before_execute = QtCore.Signal(str) + + def __init__(self, parent): + super().__init__(parent) + + code_input = PythonCodeEditor(self) + + self.setFocusProxy(code_input) + + add_tab_btn = QtWidgets.QPushButton("Add tab...", self) + add_tab_btn.setDefault(False) + add_tab_btn.setToolTip("Add new tab") + + execute_btn = QtWidgets.QPushButton("Execute", self) + execute_btn.setDefault(False) + execute_btn.setToolTip("Execute command (Ctrl + Enter)") + + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addWidget(add_tab_btn) + btns_layout.addStretch(1) + btns_layout.addWidget(execute_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(code_input, 1) + layout.addLayout(btns_layout, 0) + + add_tab_btn.clicked.connect(self._on_add_tab_clicked) + execute_btn.clicked.connect(self._on_execute_clicked) + code_input.execute_requested.connect(self.execute) + + self._code_input = code_input + self._interpreter = InteractiveInterpreter() + + def _on_add_tab_clicked(self): + self.add_tab_requested.emit() + + def _on_execute_clicked(self): + self.execute() + + def get_code(self): + return self._code_input.toPlainText() + + def set_code(self, code_text): + self._code_input.setPlainText(code_text) + + def execute(self): + code_text = self._code_input.toPlainText() + self.before_execute.emit(code_text) + self._interpreter.runcode(code_text) + + +class TabNameDialog(QtWidgets.QDialog): + default_width = 330 + default_height = 85 + + def __init__(self, parent): + super().__init__(parent) + + self.setWindowTitle("Enter tab name") + + name_label = QtWidgets.QLabel("Tab name:", self) + name_input = QtWidgets.QLineEdit(self) + + inputs_layout = QtWidgets.QHBoxLayout() + inputs_layout.addWidget(name_label) + inputs_layout.addWidget(name_input) + + ok_btn = QtWidgets.QPushButton("Ok", self) + cancel_btn = QtWidgets.QPushButton("Cancel", self) + btns_layout = QtWidgets.QHBoxLayout() + btns_layout.addStretch(1) + btns_layout.addWidget(ok_btn) + btns_layout.addWidget(cancel_btn) + + layout = QtWidgets.QVBoxLayout(self) + layout.addLayout(inputs_layout) + layout.addStretch(1) + layout.addLayout(btns_layout) + + ok_btn.clicked.connect(self._on_ok_clicked) + cancel_btn.clicked.connect(self._on_cancel_clicked) + + self._name_input = name_input + self._ok_btn = ok_btn + self._cancel_btn = cancel_btn + + self._result = None + + self.resize(self.default_width, self.default_height) + + def set_tab_name(self, name): + self._name_input.setText(name) + + def result(self): + return self._result + + def showEvent(self, event): + super().showEvent(event) + btns_width = max( + self._ok_btn.width(), + self._cancel_btn.width() + ) + + self._ok_btn.setMinimumWidth(btns_width) + self._cancel_btn.setMinimumWidth(btns_width) + + def _on_ok_clicked(self): + self._result = self._name_input.text() + self.accept() + + def _on_cancel_clicked(self): + self._result = None + self.reject() + + +class OutputTextWidget(QtWidgets.QTextEdit): + v_max_offset = 4 + + def vertical_scroll_at_max(self): + v_scroll = self.verticalScrollBar() + return v_scroll.value() > v_scroll.maximum() - self.v_max_offset + + def scroll_to_bottom(self): + v_scroll = self.verticalScrollBar() + return v_scroll.setValue(v_scroll.maximum()) + + +class EnhancedTabBar(QtWidgets.QTabBar): + double_clicked = QtCore.Signal(QtCore.QPoint) + right_clicked = QtCore.Signal(QtCore.QPoint) + mid_clicked = QtCore.Signal(QtCore.QPoint) + + def __init__(self, parent): + super().__init__(parent) + + self.setDrawBase(False) + + def mouseDoubleClickEvent(self, event): + self.double_clicked.emit(event.globalPos()) + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.right_clicked.emit(event.globalPos()) + event.accept() + return + + elif event.button() == QtCore.Qt.MidButton: + self.mid_clicked.emit(event.globalPos()) + event.accept() + + else: + super().mouseReleaseEvent(event) + diff --git a/client/ayon_core/tools/console_interpreter/ui/window.py b/client/ayon_core/tools/console_interpreter/ui/window.py new file mode 100644 index 0000000000..a5065f96f9 --- /dev/null +++ b/client/ayon_core/tools/console_interpreter/ui/window.py @@ -0,0 +1,324 @@ +import re +from typing import Optional + +from qtpy import QtWidgets, QtGui, QtCore + +from ayon_core import resources +from ayon_core.style import load_stylesheet +from ayon_core.tools.console_interpreter import ( + AbstractInterpreterController, + InterpreterController, +) + +from .utils import StdOEWrap +from .widgets import ( + PythonTabWidget, + OutputTextWidget, + EnhancedTabBar, + TabNameDialog, +) + +ANSI_ESCAPE = re.compile( + r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]" +) +AYON_ART = r""" + + ▄██▄ + ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄ + ▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███ + ▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███ + ▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀ + ▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄ + + · · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · · + +""" + + +class ConsoleInterpreterWindow(QtWidgets.QWidget): + default_width = 1000 + default_height = 600 + + def __init__( + self, + controller: Optional[AbstractInterpreterController] = None, + parent: Optional[QtWidgets.QWidget] = None, + ): + super().__init__(parent) + + self.setWindowTitle("AYON Console") + self.setWindowIcon(QtGui.QIcon(resources.get_ayon_icon_filepath())) + + if controller is None: + controller = InterpreterController() + + output_widget = OutputTextWidget(self) + output_widget.setObjectName("PythonInterpreterOutput") + output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) + output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + + tab_widget = QtWidgets.QTabWidget(self) + tab_bar = EnhancedTabBar(tab_widget) + tab_widget.setTabBar(tab_bar) + tab_widget.setTabsClosable(False) + tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + + widgets_splitter = QtWidgets.QSplitter(self) + widgets_splitter.setOrientation(QtCore.Qt.Vertical) + widgets_splitter.addWidget(output_widget) + widgets_splitter.addWidget(tab_widget) + widgets_splitter.setStretchFactor(0, 1) + widgets_splitter.setStretchFactor(1, 1) + height = int(self.default_height / 2) + widgets_splitter.setSizes([height, self.default_height - height]) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(widgets_splitter) + + line_check_timer = QtCore.QTimer() + line_check_timer.setInterval(200) + + line_check_timer.timeout.connect(self._on_timer_timeout) + tab_bar.right_clicked.connect(self._on_tab_right_click) + tab_bar.double_clicked.connect(self._on_tab_double_click) + tab_bar.mid_clicked.connect(self._on_tab_mid_click) + tab_widget.tabCloseRequested.connect(self._on_tab_close_req) + + self._tabs = [] + + self._stdout_err_wrapper = StdOEWrap() + + self._widgets_splitter = widgets_splitter + self._output_widget = output_widget + self._tab_widget = tab_widget + self._line_check_timer = line_check_timer + + self._append_lines([AYON_ART]) + + self._first_show = True + self._controller = controller + + def showEvent(self, event): + self._line_check_timer.start() + super().showEvent(event) + # First show setup + if self._first_show: + self._first_show = False + self._on_first_show() + + if self._tab_widget.count() < 1: + self.add_tab("Python") + + self._output_widget.scroll_to_bottom() + + def closeEvent(self, event): + self._save_registry() + super().closeEvent(event) + self._line_check_timer.stop() + + def add_tab(self, tab_name, index=None): + widget = PythonTabWidget(self) + widget.before_execute.connect(self._on_before_execute) + widget.add_tab_requested.connect(self._on_add_requested) + if index is None: + if self._tab_widget.count() > 0: + index = self._tab_widget.currentIndex() + 1 + else: + index = 0 + + self._tabs.append(widget) + self._tab_widget.insertTab(index, widget, tab_name) + self._tab_widget.setCurrentIndex(index) + + if self._tab_widget.count() > 1: + self._tab_widget.setTabsClosable(True) + widget.setFocus() + return widget + + def _on_first_show(self): + config = self._controller.get_config() + width = config.width + height = config.height + if width is None or width < 200: + width = self.default_width + if height is None or height < 200: + height = self.default_height + + for tab_item in config.tabs: + widget = self.add_tab(tab_item.name) + widget.set_code(tab_item.code) + + self.resize(width, height) + # Change stylesheet + self.setStyleSheet(load_stylesheet()) + # Check if splitter sizes are set + splitters_count = len(self._widgets_splitter.sizes()) + if len(config.splitter_sizes) == splitters_count: + self._widgets_splitter.setSizes(config.splitter_sizes) + + def _save_registry(self): + tabs = [] + for tab_idx in range(self._tab_widget.count()): + widget = self._tab_widget.widget(tab_idx) + tabs.append({ + "name": self._tab_widget.tabText(tab_idx), + "code": widget.get_code() + }) + + self._controller.save_config( + self.width(), + self.height(), + self._widgets_splitter.sizes(), + tabs + ) + + def _on_tab_right_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + menu = QtWidgets.QMenu(self._tab_widget) + + add_tab_action = QtWidgets.QAction("Add tab...", menu) + add_tab_action.setToolTip("Add new tab") + + rename_tab_action = QtWidgets.QAction("Rename...", menu) + rename_tab_action.setToolTip("Rename tab") + + duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu) + duplicate_tab_action.setToolTip("Duplicate code to new tab") + + close_tab_action = QtWidgets.QAction("Close", menu) + close_tab_action.setToolTip("Close tab and lose content") + close_tab_action.setEnabled(self._tab_widget.tabsClosable()) + + menu.addAction(add_tab_action) + menu.addAction(rename_tab_action) + menu.addAction(duplicate_tab_action) + menu.addAction(close_tab_action) + + result = menu.exec_(global_point) + if result is None: + return + + if result is rename_tab_action: + self._rename_tab_req(tab_idx) + + elif result is add_tab_action: + self._on_add_requested() + + elif result is duplicate_tab_action: + self._duplicate_requested(tab_idx) + + elif result is close_tab_action: + self._on_tab_close_req(tab_idx) + + def _rename_tab_req(self, tab_idx): + dialog = TabNameDialog(self) + dialog.set_tab_name(self._tab_widget.tabText(tab_idx)) + dialog.exec_() + tab_name = dialog.result() + if tab_name: + self._tab_widget.setTabText(tab_idx, tab_name) + + def _duplicate_requested(self, tab_idx=None): + if tab_idx is None: + tab_idx = self._tab_widget.currentIndex() + + src_widget = self._tab_widget.widget(tab_idx) + dst_widget = self._add_tab() + if dst_widget is None: + return + dst_widget.set_code(src_widget.get_code()) + + def _on_tab_mid_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + self._on_tab_close_req(tab_idx) + + def _on_tab_double_click(self, global_point): + point = self._tab_widget.mapFromGlobal(global_point) + tab_bar = self._tab_widget.tabBar() + tab_idx = tab_bar.tabAt(point) + last_index = tab_bar.count() - 1 + if tab_idx < 0 or tab_idx > last_index: + return + + self._rename_tab_req(tab_idx) + + def _on_tab_close_req(self, tab_index): + if self._tab_widget.count() == 1: + return + + widget = self._tab_widget.widget(tab_index) + if widget in self._tabs: + self._tabs.remove(widget) + self._tab_widget.removeTab(tab_index) + + if self._tab_widget.count() == 1: + self._tab_widget.setTabsClosable(False) + + def _append_lines(self, lines): + at_max = self._output_widget.vertical_scroll_at_max() + tmp_cursor = QtGui.QTextCursor(self._output_widget.document()) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + for line in lines: + tmp_cursor.insertText(line) + + if at_max: + self._output_widget.scroll_to_bottom() + + def _on_timer_timeout(self): + if self._stdout_err_wrapper.lines: + lines = [] + while self._stdout_err_wrapper.lines: + line = self._stdout_err_wrapper.lines.popleft() + lines.append(ANSI_ESCAPE.sub("", line)) + self._append_lines(lines) + + def _on_add_requested(self): + self._add_tab() + + def _add_tab(self): + dialog = TabNameDialog(self) + dialog.exec_() + tab_name = dialog.result() + if tab_name: + return self.add_tab(tab_name) + + return None + + def _on_before_execute(self, code_text): + at_max = self._output_widget.vertical_scroll_at_max() + document = self._output_widget.document() + tmp_cursor = QtGui.QTextCursor(document) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-")) + + code_block_format = QtGui.QTextFrameFormat() + code_block_format.setBackground(QtGui.QColor(27, 27, 27)) + code_block_format.setPadding(4) + + tmp_cursor.insertFrame(code_block_format) + char_format = tmp_cursor.charFormat() + char_format.setForeground( + QtGui.QBrush(QtGui.QColor(114, 224, 198)) + ) + tmp_cursor.setCharFormat(char_format) + tmp_cursor.insertText(code_text) + + # Create new cursor + tmp_cursor = QtGui.QTextCursor(document) + tmp_cursor.movePosition(QtGui.QTextCursor.End) + tmp_cursor.insertText("{}\n".format(20 * "-")) + + if at_max: + self._output_widget.scroll_to_bottom() From bf631d565d2bfff14409d41023d7a4f0ed3e73ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:10:52 +0100 Subject: [PATCH 383/546] add Console to default tray actions --- client/ayon_core/tools/tray/ui/tray.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index e61f903c80..638a316634 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -35,6 +35,7 @@ from ayon_core.tools.tray.lib import ( ) from ayon_core.tools.launcher.ui import LauncherWindow from ayon_core.tools.loader.ui import LoaderWindow +from ayon_core.tools.console_interpreter.ui import ConsoleInterpreterWindow from .addons_manager import TrayAddonsManager from .host_console_listener import HostListener @@ -87,6 +88,7 @@ class TrayManager: self._launcher_window = None self._browser_window = None + self._console_window = ConsoleInterpreterWindow() self._update_check_timer = update_check_timer self._update_check_interval = update_check_interval @@ -154,6 +156,11 @@ class TrayManager: tray_menu = self.tray_widget.menu + console_action = ITrayAction.add_action_to_admin_submenu( + "Console", tray_menu + ) + console_action.triggered.connect(self._show_console_window) + self._addons_manager.initialize(tray_menu) # Add default actions under addon actions @@ -563,6 +570,11 @@ class TrayManager: self._browser_window.raise_() self._browser_window.activateWindow() + def _show_console_window(self): + self._console_window.show() + self._console_window.raise_() + self._console_window.activateWindow() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From 21e60135f434b2b8f6553cc2d0aeb12e4e68049e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:11:20 +0100 Subject: [PATCH 384/546] remove 'ayon_core.modules' --- client/ayon_core/addon/base.py | 58 +- client/ayon_core/modules/__init__.py | 0 .../python_console_interpreter/__init__.py | 8 - .../python_console_interpreter/addon.py | 42 -- .../window/__init__.py | 8 - .../window/widgets.py | 660 ------------------ 6 files changed, 1 insertion(+), 775 deletions(-) delete mode 100644 client/ayon_core/modules/__init__.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/__init__.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/addon.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/window/__init__.py delete mode 100644 client/ayon_core/modules/python_console_interpreter/window/widgets.py diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 364a84cb7b..ed6b82ef52 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -370,67 +370,11 @@ def _load_ayon_addons(log): return all_addon_modules -def _load_addons_in_core(log): - # Add current directory at first place - # - has small differences in import logic - addon_modules = [] - modules_dir = os.path.join(AYON_CORE_ROOT, "modules") - if not os.path.exists(modules_dir): - log.warning( - f"Could not find path when loading AYON addons \"{modules_dir}\"" - ) - return addon_modules - - ignored_filenames = IGNORED_FILENAMES | IGNORED_DEFAULT_FILENAMES - for filename in os.listdir(modules_dir): - # Ignore filenames - if filename in ignored_filenames: - continue - - fullpath = os.path.join(modules_dir, filename) - basename, ext = os.path.splitext(filename) - - # Validations - if os.path.isdir(fullpath): - # Check existence of init file - init_path = os.path.join(fullpath, "__init__.py") - if not os.path.exists(init_path): - log.debug(( - "Addon directory does not contain __init__.py" - f" file {fullpath}" - )) - continue - - elif ext != ".py": - continue - - # TODO add more logic how to define if folder is addon or not - # - check manifest and content of manifest - try: - # Don't import dynamically current directory modules - import_str = f"ayon_core.modules.{basename}" - default_module = __import__(import_str, fromlist=("", )) - addon_modules.append(default_module) - - except Exception: - log.error( - f"Failed to import in-core addon '{basename}'.", - exc_info=True - ) - return addon_modules - - def _load_addons(): log = Logger.get_logger("AddonsLoader") - addon_modules = _load_ayon_addons(log) - # All addon in 'modules' folder are tray actions and should be moved - # to tray tool. - # TODO remove - addon_modules.extend(_load_addons_in_core(log)) - # Store modules to local cache - _LoadCache.addon_modules = addon_modules + _LoadCache.addon_modules = _load_ayon_addons(log) class AYONAddon(ABC): diff --git a/client/ayon_core/modules/__init__.py b/client/ayon_core/modules/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/modules/python_console_interpreter/__init__.py b/client/ayon_core/modules/python_console_interpreter/__init__.py deleted file mode 100644 index 8d5c23bdba..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .addon import ( - PythonInterpreterAction -) - - -__all__ = ( - "PythonInterpreterAction", -) diff --git a/client/ayon_core/modules/python_console_interpreter/addon.py b/client/ayon_core/modules/python_console_interpreter/addon.py deleted file mode 100644 index b0dce2585e..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/addon.py +++ /dev/null @@ -1,42 +0,0 @@ -from ayon_core.addon import AYONAddon, ITrayAction - - -class PythonInterpreterAction(AYONAddon, ITrayAction): - label = "Console" - name = "python_interpreter" - version = "1.0.0" - admin_action = True - - def initialize(self, settings): - self._interpreter_window = None - - def tray_init(self): - self.create_interpreter_window() - - def tray_exit(self): - if self._interpreter_window is not None: - self._interpreter_window.save_registry() - - def create_interpreter_window(self): - """Initializa Settings Qt window.""" - if self._interpreter_window: - return - - from ayon_core.modules.python_console_interpreter.window import ( - PythonInterpreterWidget - ) - - self._interpreter_window = PythonInterpreterWidget() - - def on_action_trigger(self): - self.show_interpreter_window() - - def show_interpreter_window(self): - self.create_interpreter_window() - - if self._interpreter_window.isVisible(): - self._interpreter_window.activateWindow() - self._interpreter_window.raise_() - return - - self._interpreter_window.show() diff --git a/client/ayon_core/modules/python_console_interpreter/window/__init__.py b/client/ayon_core/modules/python_console_interpreter/window/__init__.py deleted file mode 100644 index 92fd6f1df2..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/window/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from .widgets import ( - PythonInterpreterWidget -) - - -__all__ = ( - "PythonInterpreterWidget", -) diff --git a/client/ayon_core/modules/python_console_interpreter/window/widgets.py b/client/ayon_core/modules/python_console_interpreter/window/widgets.py deleted file mode 100644 index 628a2e72ff..0000000000 --- a/client/ayon_core/modules/python_console_interpreter/window/widgets.py +++ /dev/null @@ -1,660 +0,0 @@ -import os -import re -import sys -import collections -from code import InteractiveInterpreter - -import appdirs -from qtpy import QtCore, QtWidgets, QtGui - -from ayon_core import resources -from ayon_core.style import load_stylesheet -from ayon_core.lib import JSONSettingRegistry - - -ayon_art = r""" - - ▄██▄ - ▄███▄ ▀██▄ ▀██▀ ▄██▀ ▄██▀▀▀██▄ ▀███▄ █▄ - ▄▄ ▀██▄ ▀██▄ ▄██▀ ██▀ ▀██▄ ▄ ▀██▄ ███ - ▄██▀ ██▄ ▀ ▄▄ ▀ ██ ▄██ ███ ▀██▄ ███ - ▄██▀ ▀██▄ ██ ▀██▄ ▄██▀ ███ ▀██ ▀█▀ - ▄██▀ ▀██▄ ▀█ ▀██▄▄▄▄██▀ █▀ ▀██▄ - - · · - =[ by YNPUT ]:[ http://ayon.ynput.io ]= - · · - -""" - - -class PythonInterpreterRegistry(JSONSettingRegistry): - """Class handling OpenPype general settings registry. - - Attributes: - vendor (str): Name used for path construction. - product (str): Additional name used for path construction. - - """ - - def __init__(self): - self.vendor = "Ynput" - self.product = "AYON" - name = "python_interpreter_tool" - path = appdirs.user_data_dir(self.product, self.vendor) - super(PythonInterpreterRegistry, self).__init__(name, path) - - -class StdOEWrap: - def __init__(self): - self._origin_stdout_write = None - self._origin_stderr_write = None - self._listening = False - self.lines = collections.deque() - - if not sys.stdout: - sys.stdout = open(os.devnull, "w") - - if not sys.stderr: - sys.stderr = open(os.devnull, "w") - - if self._origin_stdout_write is None: - self._origin_stdout_write = sys.stdout.write - - if self._origin_stderr_write is None: - self._origin_stderr_write = sys.stderr.write - - self._listening = True - sys.stdout.write = self._stdout_listener - sys.stderr.write = self._stderr_listener - - def stop_listen(self): - self._listening = False - - def _stdout_listener(self, text): - if self._listening: - self.lines.append(text) - if self._origin_stdout_write is not None: - self._origin_stdout_write(text) - - def _stderr_listener(self, text): - if self._listening: - self.lines.append(text) - if self._origin_stderr_write is not None: - self._origin_stderr_write(text) - - -class PythonCodeEditor(QtWidgets.QPlainTextEdit): - execute_requested = QtCore.Signal() - - def __init__(self, parent): - super(PythonCodeEditor, self).__init__(parent) - - self.setObjectName("PythonCodeEditor") - - self._indent = 4 - - def _tab_shift_right(self): - cursor = self.textCursor() - selected_text = cursor.selectedText() - if not selected_text: - cursor.insertText(" " * self._indent) - return - - sel_start = cursor.selectionStart() - sel_end = cursor.selectionEnd() - cursor.setPosition(sel_end) - end_line = cursor.blockNumber() - cursor.setPosition(sel_start) - while True: - cursor.movePosition(QtGui.QTextCursor.StartOfLine) - text = cursor.block().text() - spaces = len(text) - len(text.lstrip(" ")) - new_spaces = spaces % self._indent - if not new_spaces: - new_spaces = self._indent - - cursor.insertText(" " * new_spaces) - if cursor.blockNumber() == end_line: - break - - cursor.movePosition(QtGui.QTextCursor.NextBlock) - - def _tab_shift_left(self): - tmp_cursor = self.textCursor() - sel_start = tmp_cursor.selectionStart() - sel_end = tmp_cursor.selectionEnd() - - cursor = QtGui.QTextCursor(self.document()) - cursor.setPosition(sel_end) - end_line = cursor.blockNumber() - cursor.setPosition(sel_start) - while True: - cursor.movePosition(QtGui.QTextCursor.StartOfLine) - text = cursor.block().text() - spaces = len(text) - len(text.lstrip(" ")) - if spaces: - spaces_to_remove = (spaces % self._indent) or self._indent - if spaces_to_remove > spaces: - spaces_to_remove = spaces - - cursor.setPosition( - cursor.position() + spaces_to_remove, - QtGui.QTextCursor.KeepAnchor - ) - cursor.removeSelectedText() - - if cursor.blockNumber() == end_line: - break - - cursor.movePosition(QtGui.QTextCursor.NextBlock) - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Backtab: - self._tab_shift_left() - event.accept() - return - - if event.key() == QtCore.Qt.Key_Tab: - if event.modifiers() == QtCore.Qt.NoModifier: - self._tab_shift_right() - event.accept() - return - - if ( - event.key() == QtCore.Qt.Key_Return - and event.modifiers() == QtCore.Qt.ControlModifier - ): - self.execute_requested.emit() - event.accept() - return - - super(PythonCodeEditor, self).keyPressEvent(event) - - -class PythonTabWidget(QtWidgets.QWidget): - add_tab_requested = QtCore.Signal() - before_execute = QtCore.Signal(str) - - def __init__(self, parent): - super(PythonTabWidget, self).__init__(parent) - - code_input = PythonCodeEditor(self) - - self.setFocusProxy(code_input) - - add_tab_btn = QtWidgets.QPushButton("Add tab...", self) - add_tab_btn.setToolTip("Add new tab") - - execute_btn = QtWidgets.QPushButton("Execute", self) - execute_btn.setToolTip("Execute command (Ctrl + Enter)") - - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.setContentsMargins(0, 0, 0, 0) - btns_layout.addWidget(add_tab_btn) - btns_layout.addStretch(1) - btns_layout.addWidget(execute_btn) - - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(code_input, 1) - layout.addLayout(btns_layout, 0) - - add_tab_btn.clicked.connect(self._on_add_tab_clicked) - execute_btn.clicked.connect(self._on_execute_clicked) - code_input.execute_requested.connect(self.execute) - - self._code_input = code_input - self._interpreter = InteractiveInterpreter() - - def _on_add_tab_clicked(self): - self.add_tab_requested.emit() - - def _on_execute_clicked(self): - self.execute() - - def get_code(self): - return self._code_input.toPlainText() - - def set_code(self, code_text): - self._code_input.setPlainText(code_text) - - def execute(self): - code_text = self._code_input.toPlainText() - self.before_execute.emit(code_text) - self._interpreter.runcode(code_text) - - -class TabNameDialog(QtWidgets.QDialog): - default_width = 330 - default_height = 85 - - def __init__(self, parent): - super(TabNameDialog, self).__init__(parent) - - self.setWindowTitle("Enter tab name") - - name_label = QtWidgets.QLabel("Tab name:", self) - name_input = QtWidgets.QLineEdit(self) - - inputs_layout = QtWidgets.QHBoxLayout() - inputs_layout.addWidget(name_label) - inputs_layout.addWidget(name_input) - - ok_btn = QtWidgets.QPushButton("Ok", self) - cancel_btn = QtWidgets.QPushButton("Cancel", self) - btns_layout = QtWidgets.QHBoxLayout() - btns_layout.addStretch(1) - btns_layout.addWidget(ok_btn) - btns_layout.addWidget(cancel_btn) - - layout = QtWidgets.QVBoxLayout(self) - layout.addLayout(inputs_layout) - layout.addStretch(1) - layout.addLayout(btns_layout) - - ok_btn.clicked.connect(self._on_ok_clicked) - cancel_btn.clicked.connect(self._on_cancel_clicked) - - self._name_input = name_input - self._ok_btn = ok_btn - self._cancel_btn = cancel_btn - - self._result = None - - self.resize(self.default_width, self.default_height) - - def set_tab_name(self, name): - self._name_input.setText(name) - - def result(self): - return self._result - - def showEvent(self, event): - super(TabNameDialog, self).showEvent(event) - btns_width = max( - self._ok_btn.width(), - self._cancel_btn.width() - ) - - self._ok_btn.setMinimumWidth(btns_width) - self._cancel_btn.setMinimumWidth(btns_width) - - def _on_ok_clicked(self): - self._result = self._name_input.text() - self.accept() - - def _on_cancel_clicked(self): - self._result = None - self.reject() - - -class OutputTextWidget(QtWidgets.QTextEdit): - v_max_offset = 4 - - def vertical_scroll_at_max(self): - v_scroll = self.verticalScrollBar() - return v_scroll.value() > v_scroll.maximum() - self.v_max_offset - - def scroll_to_bottom(self): - v_scroll = self.verticalScrollBar() - return v_scroll.setValue(v_scroll.maximum()) - - -class EnhancedTabBar(QtWidgets.QTabBar): - double_clicked = QtCore.Signal(QtCore.QPoint) - right_clicked = QtCore.Signal(QtCore.QPoint) - mid_clicked = QtCore.Signal(QtCore.QPoint) - - def __init__(self, parent): - super(EnhancedTabBar, self).__init__(parent) - - self.setDrawBase(False) - - def mouseDoubleClickEvent(self, event): - self.double_clicked.emit(event.globalPos()) - event.accept() - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.RightButton: - self.right_clicked.emit(event.globalPos()) - event.accept() - return - - elif event.button() == QtCore.Qt.MidButton: - self.mid_clicked.emit(event.globalPos()) - event.accept() - - else: - super(EnhancedTabBar, self).mouseReleaseEvent(event) - - -class PythonInterpreterWidget(QtWidgets.QWidget): - default_width = 1000 - default_height = 600 - - def __init__(self, allow_save_registry=True, parent=None): - super(PythonInterpreterWidget, self).__init__(parent) - - self.setWindowTitle("AYON Console") - self.setWindowIcon(QtGui.QIcon(resources.get_ayon_icon_filepath())) - - self.ansi_escape = re.compile( - r"(?:\x1B[@-_]|[\x80-\x9F])[0-?]*[ -/]*[@-~]" - ) - - self._tabs = [] - - self._stdout_err_wrapper = StdOEWrap() - - output_widget = OutputTextWidget(self) - output_widget.setObjectName("PythonInterpreterOutput") - output_widget.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) - output_widget.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - - tab_widget = QtWidgets.QTabWidget(self) - tab_bar = EnhancedTabBar(tab_widget) - tab_widget.setTabBar(tab_bar) - tab_widget.setTabsClosable(False) - tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - - widgets_splitter = QtWidgets.QSplitter(self) - widgets_splitter.setOrientation(QtCore.Qt.Vertical) - widgets_splitter.addWidget(output_widget) - widgets_splitter.addWidget(tab_widget) - widgets_splitter.setStretchFactor(0, 1) - widgets_splitter.setStretchFactor(1, 1) - height = int(self.default_height / 2) - widgets_splitter.setSizes([height, self.default_height - height]) - - layout = QtWidgets.QVBoxLayout(self) - layout.addWidget(widgets_splitter) - - line_check_timer = QtCore.QTimer() - line_check_timer.setInterval(200) - - line_check_timer.timeout.connect(self._on_timer_timeout) - tab_bar.right_clicked.connect(self._on_tab_right_click) - tab_bar.double_clicked.connect(self._on_tab_double_click) - tab_bar.mid_clicked.connect(self._on_tab_mid_click) - tab_widget.tabCloseRequested.connect(self._on_tab_close_req) - - self._widgets_splitter = widgets_splitter - self._output_widget = output_widget - self._tab_widget = tab_widget - self._line_check_timer = line_check_timer - - self._append_lines([ayon_art]) - - self._first_show = True - self._splitter_size_ratio = None - self._allow_save_registry = allow_save_registry - self._registry_saved = True - - self._init_from_registry() - - if self._tab_widget.count() < 1: - self.add_tab("Python") - - def _init_from_registry(self): - setting_registry = PythonInterpreterRegistry() - width = None - height = None - try: - width = setting_registry.get_item("width") - height = setting_registry.get_item("height") - - except ValueError: - pass - - if width is None or width < 200: - width = self.default_width - - if height is None or height < 200: - height = self.default_height - - self.resize(width, height) - - try: - self._splitter_size_ratio = ( - setting_registry.get_item("splitter_sizes") - ) - - except ValueError: - pass - - try: - tab_defs = setting_registry.get_item("tabs") or [] - for tab_def in tab_defs: - widget = self.add_tab(tab_def["name"]) - widget.set_code(tab_def["code"]) - - except ValueError: - pass - - def save_registry(self): - # Window was not showed - if not self._allow_save_registry or self._registry_saved: - return - - self._registry_saved = True - setting_registry = PythonInterpreterRegistry() - - setting_registry.set_item("width", self.width()) - setting_registry.set_item("height", self.height()) - - setting_registry.set_item( - "splitter_sizes", self._widgets_splitter.sizes() - ) - - tabs = [] - for tab_idx in range(self._tab_widget.count()): - widget = self._tab_widget.widget(tab_idx) - tab_code = widget.get_code() - tab_name = self._tab_widget.tabText(tab_idx) - tabs.append({ - "name": tab_name, - "code": tab_code - }) - - setting_registry.set_item("tabs", tabs) - - def _on_tab_right_click(self, global_point): - point = self._tab_widget.mapFromGlobal(global_point) - tab_bar = self._tab_widget.tabBar() - tab_idx = tab_bar.tabAt(point) - last_index = tab_bar.count() - 1 - if tab_idx < 0 or tab_idx > last_index: - return - - menu = QtWidgets.QMenu(self._tab_widget) - - add_tab_action = QtWidgets.QAction("Add tab...", menu) - add_tab_action.setToolTip("Add new tab") - - rename_tab_action = QtWidgets.QAction("Rename...", menu) - rename_tab_action.setToolTip("Rename tab") - - duplicate_tab_action = QtWidgets.QAction("Duplicate...", menu) - duplicate_tab_action.setToolTip("Duplicate code to new tab") - - close_tab_action = QtWidgets.QAction("Close", menu) - close_tab_action.setToolTip("Close tab and lose content") - close_tab_action.setEnabled(self._tab_widget.tabsClosable()) - - menu.addAction(add_tab_action) - menu.addAction(rename_tab_action) - menu.addAction(duplicate_tab_action) - menu.addAction(close_tab_action) - - result = menu.exec_(global_point) - if result is None: - return - - if result is rename_tab_action: - self._rename_tab_req(tab_idx) - - elif result is add_tab_action: - self._on_add_requested() - - elif result is duplicate_tab_action: - self._duplicate_requested(tab_idx) - - elif result is close_tab_action: - self._on_tab_close_req(tab_idx) - - def _rename_tab_req(self, tab_idx): - dialog = TabNameDialog(self) - dialog.set_tab_name(self._tab_widget.tabText(tab_idx)) - dialog.exec_() - tab_name = dialog.result() - if tab_name: - self._tab_widget.setTabText(tab_idx, tab_name) - - def _duplicate_requested(self, tab_idx=None): - if tab_idx is None: - tab_idx = self._tab_widget.currentIndex() - - src_widget = self._tab_widget.widget(tab_idx) - dst_widget = self._add_tab() - if dst_widget is None: - return - dst_widget.set_code(src_widget.get_code()) - - def _on_tab_mid_click(self, global_point): - point = self._tab_widget.mapFromGlobal(global_point) - tab_bar = self._tab_widget.tabBar() - tab_idx = tab_bar.tabAt(point) - last_index = tab_bar.count() - 1 - if tab_idx < 0 or tab_idx > last_index: - return - - self._on_tab_close_req(tab_idx) - - def _on_tab_double_click(self, global_point): - point = self._tab_widget.mapFromGlobal(global_point) - tab_bar = self._tab_widget.tabBar() - tab_idx = tab_bar.tabAt(point) - last_index = tab_bar.count() - 1 - if tab_idx < 0 or tab_idx > last_index: - return - - self._rename_tab_req(tab_idx) - - def _on_tab_close_req(self, tab_index): - if self._tab_widget.count() == 1: - return - - widget = self._tab_widget.widget(tab_index) - if widget in self._tabs: - self._tabs.remove(widget) - self._tab_widget.removeTab(tab_index) - - if self._tab_widget.count() == 1: - self._tab_widget.setTabsClosable(False) - - def _append_lines(self, lines): - at_max = self._output_widget.vertical_scroll_at_max() - tmp_cursor = QtGui.QTextCursor(self._output_widget.document()) - tmp_cursor.movePosition(QtGui.QTextCursor.End) - for line in lines: - tmp_cursor.insertText(line) - - if at_max: - self._output_widget.scroll_to_bottom() - - def _on_timer_timeout(self): - if self._stdout_err_wrapper.lines: - lines = [] - while self._stdout_err_wrapper.lines: - line = self._stdout_err_wrapper.lines.popleft() - lines.append(self.ansi_escape.sub("", line)) - self._append_lines(lines) - - def _on_add_requested(self): - self._add_tab() - - def _add_tab(self): - dialog = TabNameDialog(self) - dialog.exec_() - tab_name = dialog.result() - if tab_name: - return self.add_tab(tab_name) - - return None - - def _on_before_execute(self, code_text): - at_max = self._output_widget.vertical_scroll_at_max() - document = self._output_widget.document() - tmp_cursor = QtGui.QTextCursor(document) - tmp_cursor.movePosition(QtGui.QTextCursor.End) - tmp_cursor.insertText("{}\nExecuting command:\n".format(20 * "-")) - - code_block_format = QtGui.QTextFrameFormat() - code_block_format.setBackground(QtGui.QColor(27, 27, 27)) - code_block_format.setPadding(4) - - tmp_cursor.insertFrame(code_block_format) - char_format = tmp_cursor.charFormat() - char_format.setForeground( - QtGui.QBrush(QtGui.QColor(114, 224, 198)) - ) - tmp_cursor.setCharFormat(char_format) - tmp_cursor.insertText(code_text) - - # Create new cursor - tmp_cursor = QtGui.QTextCursor(document) - tmp_cursor.movePosition(QtGui.QTextCursor.End) - tmp_cursor.insertText("{}\n".format(20 * "-")) - - if at_max: - self._output_widget.scroll_to_bottom() - - def add_tab(self, tab_name, index=None): - widget = PythonTabWidget(self) - widget.before_execute.connect(self._on_before_execute) - widget.add_tab_requested.connect(self._on_add_requested) - if index is None: - if self._tab_widget.count() > 0: - index = self._tab_widget.currentIndex() + 1 - else: - index = 0 - - self._tabs.append(widget) - self._tab_widget.insertTab(index, widget, tab_name) - self._tab_widget.setCurrentIndex(index) - - if self._tab_widget.count() > 1: - self._tab_widget.setTabsClosable(True) - widget.setFocus() - return widget - - def showEvent(self, event): - self._line_check_timer.start() - self._registry_saved = False - super(PythonInterpreterWidget, self).showEvent(event) - # First show setup - if self._first_show: - self._first_show = False - self._on_first_show() - - self._output_widget.scroll_to_bottom() - - def _on_first_show(self): - # Change stylesheet - self.setStyleSheet(load_stylesheet()) - # Check if splitter size ratio is set - # - first store value to local variable and then unset it - splitter_size_ratio = self._splitter_size_ratio - self._splitter_size_ratio = None - # Skip if is not set - if not splitter_size_ratio: - return - - # Skip if number of size items does not match to splitter - splitters_count = len(self._widgets_splitter.sizes()) - if len(splitter_size_ratio) == splitters_count: - self._widgets_splitter.setSizes(splitter_size_ratio) - - def closeEvent(self, event): - self.save_registry() - super(PythonInterpreterWidget, self).closeEvent(event) - self._line_check_timer.stop() From a8441e3036816e6fe3cb44239e4bc3cdc8c8b4a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:14:51 +0100 Subject: [PATCH 385/546] enhanced admin menu options --- client/ayon_core/addon/interfaces.py | 40 +++++++++++++++----------- client/ayon_core/tools/tray/ui/tray.py | 6 ++-- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index b273e7839b..2616913dc0 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -125,6 +125,7 @@ class ITrayAddon(AYONInterface): tray_initialized = False _tray_manager = None + _admin_submenu = None @abstractmethod def tray_init(self): @@ -198,6 +199,27 @@ class ITrayAddon(AYONInterface): if hasattr(self.manager, "add_doubleclick_callback"): self.manager.add_doubleclick_callback(self, callback) + @staticmethod + def admin_submenu(tray_menu): + if ITrayAddon._admin_submenu is None: + from qtpy import QtWidgets + + admin_submenu = QtWidgets.QMenu("Admin", tray_menu) + admin_submenu.menuAction().setVisible(False) + ITrayAddon._admin_submenu = admin_submenu + return ITrayAddon._admin_submenu + + @staticmethod + def add_action_to_admin_submenu(label, tray_menu): + from qtpy import QtWidgets + + menu = ITrayAddon.admin_submenu(tray_menu) + action = QtWidgets.QAction(label, menu) + menu.addAction(action) + if not menu.menuAction().isVisible(): + menu.menuAction().setVisible(True) + return action + class ITrayAction(ITrayAddon): """Implementation of Tray action. @@ -211,7 +233,6 @@ class ITrayAction(ITrayAddon): """ admin_action = False - _admin_submenu = None _action_item = None @property @@ -229,12 +250,7 @@ class ITrayAction(ITrayAddon): from qtpy import QtWidgets if self.admin_action: - menu = self.admin_submenu(tray_menu) - action = QtWidgets.QAction(self.label, menu) - menu.addAction(action) - if not menu.menuAction().isVisible(): - menu.menuAction().setVisible(True) - + action = self.add_action_to_admin_submenu(self.label, tray_menu) else: action = QtWidgets.QAction(self.label, tray_menu) tray_menu.addAction(action) @@ -248,16 +264,6 @@ class ITrayAction(ITrayAddon): def tray_exit(self): return - @staticmethod - def admin_submenu(tray_menu): - if ITrayAction._admin_submenu is None: - from qtpy import QtWidgets - - admin_submenu = QtWidgets.QMenu("Admin", tray_menu) - admin_submenu.menuAction().setVisible(False) - ITrayAction._admin_submenu = admin_submenu - return ITrayAction._admin_submenu - class ITrayService(ITrayAddon): # Module's property diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 638a316634..dbaf13dfe9 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -20,7 +20,7 @@ from ayon_core.lib import ( ) from ayon_core.settings import get_studio_settings from ayon_core.addon import ( - ITrayAction, + ITrayAddon, ITrayService, ) from ayon_core.pipeline import install_ayon_plugins @@ -156,7 +156,7 @@ class TrayManager: tray_menu = self.tray_widget.menu - console_action = ITrayAction.add_action_to_admin_submenu( + console_action = ITrayAddon.add_action_to_admin_submenu( "Console", tray_menu ) console_action.triggered.connect(self._show_console_window) @@ -183,7 +183,7 @@ class TrayManager: "POST", "/tray/message", self._web_show_tray_message ) - admin_submenu = ITrayAction.admin_submenu(tray_menu) + admin_submenu = ITrayAddon.admin_submenu(tray_menu) tray_menu.addMenu(admin_submenu) # Add services if they are From 14d4c75a123b203f2d27a73316d122fea88426b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:24:20 +0100 Subject: [PATCH 386/546] add publish report viewer to admin actions --- .../publisher/publish_report_viewer/window.py | 70 +++++++++++-------- client/ayon_core/tools/tray/ui/tray.py | 17 +++++ 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/client/ayon_core/tools/publisher/publish_report_viewer/window.py b/client/ayon_core/tools/publisher/publish_report_viewer/window.py index 6921c5d162..77db65588a 100644 --- a/client/ayon_core/tools/publisher/publish_report_viewer/window.py +++ b/client/ayon_core/tools/publisher/publish_report_viewer/window.py @@ -484,6 +484,34 @@ class LoadedFilesView(QtWidgets.QTreeView): self._time_delegate = time_delegate self._remove_btn = remove_btn + def showEvent(self, event): + super().showEvent(event) + self._model.refresh() + header = self.header() + header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) + self._update_remove_btn() + + def resizeEvent(self, event): + super().resizeEvent(event) + self._update_remove_btn() + + def add_filepaths(self, filepaths): + self._model.add_filepaths(filepaths) + self._fill_selection() + + def remove_item_by_id(self, item_id): + self._model.remove_item_by_id(item_id) + self._fill_selection() + + def get_current_report(self): + index = self.currentIndex() + item_id = index.data(ITEM_ID_ROLE) + return self._model.get_report_by_id(item_id) + + def refresh(self): + self._model.refresh() + self._fill_selection() + def _update_remove_btn(self): viewport = self.viewport() height = viewport.height() + self.header().height() @@ -496,28 +524,9 @@ class LoadedFilesView(QtWidgets.QTreeView): header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) self._update_remove_btn() - def resizeEvent(self, event): - super().resizeEvent(event) - self._update_remove_btn() - - def showEvent(self, event): - super().showEvent(event) - self._model.refresh() - header = self.header() - header.resizeSections(QtWidgets.QHeaderView.ResizeToContents) - self._update_remove_btn() - def _on_selection_change(self): self.selection_changed.emit() - def add_filepaths(self, filepaths): - self._model.add_filepaths(filepaths) - self._fill_selection() - - def remove_item_by_id(self, item_id): - self._model.remove_item_by_id(item_id) - self._fill_selection() - def _on_remove_clicked(self): index = self.currentIndex() item_id = index.data(ITEM_ID_ROLE) @@ -533,11 +542,6 @@ class LoadedFilesView(QtWidgets.QTreeView): if index.isValid(): self.setCurrentIndex(index) - def get_current_report(self): - index = self.currentIndex() - item_id = index.data(ITEM_ID_ROLE) - return self._model.get_report_by_id(item_id) - class LoadedFilesWidget(QtWidgets.QWidget): report_changed = QtCore.Signal() @@ -577,15 +581,18 @@ class LoadedFilesWidget(QtWidgets.QWidget): self._add_filepaths(filepaths) event.accept() + def refresh(self): + self._view.refresh() + + def get_current_report(self): + return self._view.get_current_report() + def _on_report_change(self): self.report_changed.emit() def _add_filepaths(self, filepaths): self._view.add_filepaths(filepaths) - def get_current_report(self): - return self._view.get_current_report() - class PublishReportViewerWindow(QtWidgets.QWidget): default_width = 1200 @@ -624,9 +631,12 @@ class PublishReportViewerWindow(QtWidgets.QWidget): self.resize(self.default_width, self.default_height) self.setStyleSheet(style.load_stylesheet()) - def _on_report_change(self): - report = self._loaded_files_widget.get_current_report() - self.set_report(report) + def refresh(self): + self._loaded_files_widget.refresh() def set_report(self, report_data): self._main_widget.set_report(report_data) + + def _on_report_change(self): + report = self._loaded_files_widget.get_current_report() + self.set_report(report) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index dbaf13dfe9..98e3c783c4 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -36,6 +36,9 @@ from ayon_core.tools.tray.lib import ( from ayon_core.tools.launcher.ui import LauncherWindow from ayon_core.tools.loader.ui import LoaderWindow from ayon_core.tools.console_interpreter.ui import ConsoleInterpreterWindow +from ayon_core.tools.publisher.publish_report_viewer import ( + PublishReportViewerWindow, +) from .addons_manager import TrayAddonsManager from .host_console_listener import HostListener @@ -89,6 +92,7 @@ class TrayManager: self._launcher_window = None self._browser_window = None self._console_window = ConsoleInterpreterWindow() + self._publish_report_viewer_window = PublishReportViewerWindow() self._update_check_timer = update_check_timer self._update_check_interval = update_check_interval @@ -161,6 +165,13 @@ class TrayManager: ) console_action.triggered.connect(self._show_console_window) + publish_report_viewer_action = ITrayAddon.add_action_to_admin_submenu( + "Publish report viewer", tray_menu + ) + publish_report_viewer_action.triggered.connect( + self._show_publish_report_viewer + ) + self._addons_manager.initialize(tray_menu) # Add default actions under addon actions @@ -575,6 +586,12 @@ class TrayManager: self._console_window.raise_() self._console_window.activateWindow() + def _show_publish_report_viewer(self): + self._publish_report_viewer_window.refresh() + self._publish_report_viewer_window.show() + self._publish_report_viewer_window.raise_() + self._publish_report_viewer_window.activateWindow() + class SystemTrayIcon(QtWidgets.QSystemTrayIcon): """Tray widget. From b995c51f1cf4845ca3b9ac7f4bce68cad9fbbd17 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:38:39 +0100 Subject: [PATCH 387/546] small ux improvements in push to library project action --- .../tools/push_to_project/ui/window.py | 103 +++++++++++++++++- 1 file changed, 100 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 4d64509afd..0f2537db06 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -14,6 +14,62 @@ from ayon_core.tools.push_to_project.control import ( ) +class ErrorDetailDialog(QtWidgets.QDialog): + def __init__(self, parent): + super().__init__(parent) + + self.setWindowTitle("Error detail") + self.setWindowIcon(QtGui.QIcon(get_app_icon_path())) + + title_label = QtWidgets.QLabel(self) + + sep_1 = SeparatorWidget(parent=self) + + detail_widget = QtWidgets.QTextBrowser(self) + detail_widget.setReadOnly(True) + detail_widget.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) + + sep_2 = SeparatorWidget(parent=self) + + btns_widget = QtWidgets.QWidget(self) + + copy_btn = QtWidgets.QPushButton("Copy", btns_widget) + close_btn = QtWidgets.QPushButton("Close", btns_widget) + + btns_layout = QtWidgets.QHBoxLayout(btns_widget) + btns_layout.setContentsMargins(0, 0, 0, 0) + btns_layout.addStretch(1) + btns_layout.addWidget(copy_btn, 0) + btns_layout.addWidget(close_btn, 0) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(6, 6, 6, 6) + main_layout.addWidget(title_label, 0) + main_layout.addWidget(sep_1, 0) + main_layout.addWidget(detail_widget, 1) + main_layout.addWidget(sep_2, 0) + main_layout.addWidget(btns_widget, 0) + + copy_btn.clicked.connect(self._on_copy_click) + close_btn.clicked.connect(self._on_close_click) + + self._title_label = title_label + self._detail_widget = detail_widget + + def set_detail(self, title, detail): + self._title_label.setText(title) + self._detail_widget.setText(detail) + + def _on_copy_click(self): + clipboard = QtWidgets.QApplication.clipboard() + clipboard.setText(self._detail_widget.toPlainText()) + + def _on_close_click(self): + self.close() + + class PushToContextSelectWindow(QtWidgets.QWidget): def __init__(self, controller=None): super(PushToContextSelectWindow, self).__init__() @@ -113,6 +169,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): overlay_label = QtWidgets.QLabel(overlay_widget) overlay_label.setAlignment(QtCore.Qt.AlignCenter) + overlay_label.setWordWrap(True) + overlay_label.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction + ) overlay_btns_widget = QtWidgets.QWidget(overlay_widget) overlay_btns_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -121,13 +181,28 @@ class PushToContextSelectWindow(QtWidgets.QWidget): overlay_try_btn = QtWidgets.QPushButton( "Try again", overlay_btns_widget ) + overlay_try_btn.setToolTip( + "Hide overlay and modify submit information." + ) + + show_detail_btn = QtWidgets.QPushButton( + "Show error detail", overlay_btns_widget + ) + show_detail_btn.setToolTip( + "Show error detail dialog to copy full error." + ) + overlay_close_btn = QtWidgets.QPushButton( "Close", overlay_btns_widget ) + overlay_close_btn.setToolTip("Discard changes and close window.") overlay_btns_layout = QtWidgets.QHBoxLayout(overlay_btns_widget) + overlay_btns_layout.setContentsMargins(0, 0, 0, 0) + overlay_btns_layout.setSpacing(10) overlay_btns_layout.addStretch(1) overlay_btns_layout.addWidget(overlay_try_btn, 0) + overlay_btns_layout.addWidget(show_detail_btn, 0) overlay_btns_layout.addWidget(overlay_close_btn, 0) overlay_btns_layout.addStretch(1) @@ -162,6 +237,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) + show_detail_btn.clicked.connect(self._on_show_detail_click) overlay_close_btn.clicked.connect(self._on_close_click) overlay_try_btn.clicked.connect(self._on_try_again_click) @@ -209,10 +285,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._publish_btn = publish_btn self._overlay_widget = overlay_widget + self._show_detail_btn = show_detail_btn self._overlay_close_btn = overlay_close_btn self._overlay_try_btn = overlay_try_btn self._overlay_label = overlay_label + self._error_detail_dialog = ErrorDetailDialog(self) + self._user_input_changed_timer = user_input_changed_timer # Store current value on input text change # The value is unset when is passed to controller @@ -235,6 +314,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._folder_is_valid = None publish_btn.setEnabled(False) + show_detail_btn.setVisible(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) @@ -374,6 +454,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_submission_change(self, event): self._publish_btn.setEnabled(event["enabled"]) + def _on_show_detail_click(self): + self._error_detail_dialog.show() + def _on_close_click(self): self.close() @@ -384,8 +467,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._process_item_id = None self._last_submit_message = None + self._error_detail_dialog.close() + self._overlay_close_btn.setVisible(False) self._overlay_try_btn.setVisible(False) + self._show_detail_btn.setVisible(False) self._main_layout.setCurrentWidget(self._main_context_widget) def _on_main_thread_timer(self): @@ -401,13 +487,24 @@ class PushToContextSelectWindow(QtWidgets.QWidget): if self._main_thread_timer_can_stop: self._main_thread_timer.stop() self._overlay_close_btn.setVisible(True) - if push_failed and not fail_traceback: + if push_failed: self._overlay_try_btn.setVisible(True) + if fail_traceback: + self._show_detail_btn.setVisible(True) if push_failed: - message = "Push Failed:\n{}".format(process_status["fail_reason"]) + reason = process_status["fail_reason"] if fail_traceback: - message += "\n{}".format(fail_traceback) + message = ( + "Unhandled error happened." + " Check error detail for more information." + ) + self._error_detail_dialog.set_detail( + reason, fail_traceback + ) + else: + message = f"Push Failed:\n{reason}" + self._overlay_label.setText(message) set_style_property(self._overlay_close_btn, "state", "error") From 5d91c9ba98915ac30f859aadd202ca1f09f0e728 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:39:05 +0100 Subject: [PATCH 388/546] capture 'TaskNotSetError' --- .../tools/push_to_project/models/integrate.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index ba603699bc..32aa562a7b 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -26,7 +26,7 @@ from ayon_core.pipeline import Anatomy from ayon_core.pipeline.version_start import get_versioning_start from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.publish import get_publish_template_name -from ayon_core.pipeline.create import get_product_name +from ayon_core.pipeline.create import get_product_name, TaskNotSetError UNKNOWN = object() @@ -823,15 +823,23 @@ class ProjectPushItemProcess: task_name = task_info["name"] task_type = task_info["taskType"] - product_name = get_product_name( - self._item.dst_project_name, - task_name, - task_type, - self.host_name, - product_type, - self._item.variant, - project_settings=self._project_settings - ) + try: + product_name = get_product_name( + self._item.dst_project_name, + task_name, + task_type, + self.host_name, + product_type, + self._item.variant, + project_settings=self._project_settings + ) + except TaskNotSetError: + self._status.set_failed( + "Product name template requires task name." + " Please select target task to continue." + ) + raise PushToProjectError(self._status.fail_reason) + self._log_info( f"Push will be integrating to product with name '{product_name}'" ) From 4010183250c512c00e6fa816bd5f2ef44d76f339 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:46:27 +0100 Subject: [PATCH 389/546] bigger margins for dialog --- client/ayon_core/tools/push_to_project/ui/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 0f2537db06..94dda58916 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -45,7 +45,7 @@ class ErrorDetailDialog(QtWidgets.QDialog): btns_layout.addWidget(close_btn, 0) main_layout = QtWidgets.QVBoxLayout(self) - main_layout.setContentsMargins(6, 6, 6, 6) + main_layout.setContentsMargins(10, 10, 10, 10) main_layout.addWidget(title_label, 0) main_layout.addWidget(sep_1, 0) main_layout.addWidget(detail_widget, 1) From 69cbbeb6a7d3371bd8421de6c6cbec6738f92aaf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Dec 2024 10:37:38 +0100 Subject: [PATCH 390/546] better message --- client/ayon_core/tools/push_to_project/models/integrate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 32aa562a7b..4fe4ead9df 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -835,8 +835,10 @@ class ProjectPushItemProcess: ) except TaskNotSetError: self._status.set_failed( - "Product name template requires task name." - " Please select target task to continue." + "Target product name template requires task name. To continue" + " you have to select target task or change settings" + " `ayon+settings://core/tools/publish/template_name_profiles" + f"?project={self._item.dst_project_name}`." ) raise PushToProjectError(self._status.fail_reason) From 6f8af3f65ee73ec4d81a7f954ce18b5862399cd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:36:41 +0100 Subject: [PATCH 391/546] fix settings path --- client/ayon_core/tools/push_to_project/models/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 4fe4ead9df..6bd4279219 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -837,8 +837,8 @@ class ProjectPushItemProcess: self._status.set_failed( "Target product name template requires task name. To continue" " you have to select target task or change settings" - " `ayon+settings://core/tools/publish/template_name_profiles" - f"?project={self._item.dst_project_name}`." + " ayon+settings://core/tools/creator/product_name_profiles" + f"?project={self._item.dst_project_name}." ) raise PushToProjectError(self._status.fail_reason) From fa9e53e159a434433d027bae497f592113fb076c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:02:26 +0100 Subject: [PATCH 392/546] added checkbox to create new folder --- .../tools/push_to_project/control.py | 2 +- .../push_to_project/models/user_values.py | 7 ++-- .../tools/push_to_project/ui/window.py | 32 ++++++++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 58447a8389..fb080d158b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -321,7 +321,7 @@ class PushToContextController: return False if ( - not self._user_values.new_folder_name + self._user_values.new_folder_name is None and not self._selection_model.get_selected_folder_id() ): return False diff --git a/client/ayon_core/tools/push_to_project/models/user_values.py b/client/ayon_core/tools/push_to_project/models/user_values.py index edef2fe4fb..e52cb2917c 100644 --- a/client/ayon_core/tools/push_to_project/models/user_values.py +++ b/client/ayon_core/tools/push_to_project/models/user_values.py @@ -84,8 +84,11 @@ class UserPublishValuesModel: return self._new_folder_name = folder_name - is_valid = True - if folder_name: + if folder_name is None: + is_valid = True + elif not folder_name: + is_valid = False + else: is_valid = ( self.folder_name_regex.match(folder_name) is not None ) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 94dda58916..a69c512fcd 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -8,6 +8,7 @@ from ayon_core.tools.utils import ( ProjectsCombobox, FoldersWidget, TasksWidget, + NiceCheckbox, ) from ayon_core.tools.push_to_project.control import ( PushToContextController, @@ -122,9 +123,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # --- Inputs widget --- inputs_widget = QtWidgets.QWidget(main_splitter) + new_folder_checkbox = NiceCheckbox(True, parent=inputs_widget) + folder_name_input = PlaceholderLineEdit(inputs_widget) folder_name_input.setPlaceholderText("< Name of new folder >") folder_name_input.setObjectName("ValidatedLineEdit") + folder_name_input.setEnabled(new_folder_checkbox.isChecked()) variant_input = PlaceholderLineEdit(inputs_widget) variant_input.setPlaceholderText("< Variant >") @@ -135,6 +139,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout = QtWidgets.QFormLayout(inputs_widget) inputs_layout.setContentsMargins(0, 0, 0, 0) + inputs_layout.addRow("Create new folder", new_folder_checkbox) inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) inputs_layout.addRow("Comment", comment_input) @@ -231,6 +236,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): main_thread_timer.timeout.connect(self._on_main_thread_timer) show_timer.timeout.connect(self._on_show_timer) user_input_changed_timer.timeout.connect(self._on_user_input_timer) + new_folder_checkbox.stateChanged.connect(self._on_new_folder_check) folder_name_input.textChanged.connect(self._on_new_folder_change) variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) @@ -279,6 +285,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._tasks_widget = tasks_widget self._variant_input = variant_input + self._new_folder_checkbox = new_folder_checkbox self._folder_name_input = folder_name_input self._comment_input = comment_input @@ -297,8 +304,9 @@ class PushToContextSelectWindow(QtWidgets.QWidget): # The value is unset when is passed to controller # The goal is to have controll over changes happened during user change # in UI and controller auto-changes - self._variant_input_text = None + self._new_folder_name_enabled = None self._new_folder_name_input_text = None + self._variant_input_text = None self._comment_input_text = None self._first_show = True @@ -369,6 +377,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.refresh() + def _on_new_folder_check(self): + self._new_folder_name_enabled = self._new_folder_checkbox.isChecked() + self._folder_name_input.setEnabled(self._new_folder_name_enabled) + self._user_input_changed_timer.start() + def _on_new_folder_change(self, text): self._new_folder_name_input_text = text self._user_input_changed_timer.start() @@ -382,9 +395,15 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._user_input_changed_timer.start() def _on_user_input_timer(self): + folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text - if folder_name is not None: + if folder_name is not None or folder_name_enabled is not None: self._new_folder_name_input_text = None + self._new_folder_name_enabled = None + if not self._new_folder_checkbox.isChecked(): + folder_name = None + elif folder_name is None: + folder_name = self._folder_name_input.text() self._controller.set_user_value_folder_name(folder_name) variant = self._variant_input_text @@ -430,16 +449,13 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._header_label.setText(self._controller.get_source_label()) def _invalidate_new_folder_name(self, folder_name, is_valid): - self._tasks_widget.setVisible(not folder_name) + self._tasks_widget.setVisible(folder_name is None) if self._folder_is_valid is is_valid: return self._folder_is_valid = is_valid state = "" - if folder_name: - if is_valid is True: - state = "valid" - elif is_valid is False: - state = "invalid" + if folder_name is not None: + state = "valid" if is_valid else "invalid" set_style_property( self._folder_name_input, "state", state ) From f29f8748af94b21112802338eafe3c7fba9ec62d Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Wed, 11 Dec 2024 10:50:37 -0500 Subject: [PATCH 393/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/tempdir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index 7fb539bf0b..cd7db852a1 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -71,8 +71,8 @@ def _create_local_staging_dir(prefix, suffix, dirpath=None): ) -def create_custom_tempdir(project_name, anatomy): - """ Deprecated 09/12/2024, here for backward-compatibility with Resolve. +def create_custom_tempdir(project_name, anatomy=None): + """Backward compatibility deprecated since 2024/12/09. """ warnings.warn( "Used deprecated 'create_custom_tempdir' " From 46fcc29af138d980857adf3198408f473b6fa1e6 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 11 Dec 2024 10:59:57 -0500 Subject: [PATCH 394/546] Address feedback from PR. --- client/ayon_core/pipeline/tempdir.py | 3 +++ .../ayon_core/plugins/publish/collect_otio_subset_resources.py | 1 + 2 files changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/tempdir.py b/client/ayon_core/pipeline/tempdir.py index cd7db852a1..38b03f5c85 100644 --- a/client/ayon_core/pipeline/tempdir.py +++ b/client/ayon_core/pipeline/tempdir.py @@ -80,6 +80,9 @@ def create_custom_tempdir(project_name, anatomy=None): DeprecationWarning, ) + if anatomy is None: + anatomy = Anatomy(project_name) + return _create_custom_tempdir(project_name, anatomy) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 2d8e91fe09..10a7d53971 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -157,6 +157,7 @@ class CollectOtioSubsetResources( self.staging_dir = media_ref.target_url_base head = media_ref.name_prefix tail = media_ref.name_suffix + import rpdb ; rpdb.Rpdb().set_trace() collection = clique.Collection( head=head, tail=tail, From 80057ebf8a37bd551c5280846566ebb9bf48292e Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 11 Dec 2024 11:04:06 -0500 Subject: [PATCH 395/546] Fix lint. --- .../ayon_core/plugins/publish/collect_otio_subset_resources.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 10a7d53971..2d8e91fe09 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -157,7 +157,6 @@ class CollectOtioSubsetResources( self.staging_dir = media_ref.target_url_base head = media_ref.name_prefix tail = media_ref.name_suffix - import rpdb ; rpdb.Rpdb().set_trace() collection = clique.Collection( head=head, tail=tail, From cb39512b868a5960e7b18eea5015c004be8d531c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 12 Dec 2024 13:44:26 +0200 Subject: [PATCH 396/546] add houdini to thumbnail extraction --- client/ayon_core/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 37bbac8898..8ae18f4abf 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -37,7 +37,8 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "substancepainter", "nuke", "aftereffects", - "unreal" + "unreal", + "houdini" ] enabled = False From 40e5a4a3ade8f2062d7c7944b3c78e77f740d943 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Dec 2024 13:44:09 +0100 Subject: [PATCH 397/546] move launcher to the top --- client/ayon_core/tools/tray/ui/tray.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index 98e3c783c4..aad89b6081 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -159,6 +159,12 @@ class TrayManager: return tray_menu = self.tray_widget.menu + # Add launcher at first place + launcher_action = QtWidgets.QAction( + "Launcher", tray_menu + ) + launcher_action.triggered.connect(self._show_launcher_window) + tray_menu.addAction(launcher_action) console_action = ITrayAddon.add_action_to_admin_submenu( "Console", tray_menu @@ -174,13 +180,7 @@ class TrayManager: self._addons_manager.initialize(tray_menu) - # Add default actions under addon actions - launcher_action = QtWidgets.QAction( - "Launcher", tray_menu - ) - launcher_action.triggered.connect(self._show_launcher_window) - tray_menu.addAction(launcher_action) - + # Add browser action after addon actions browser_action = QtWidgets.QAction( "Browser", tray_menu ) From bf0f7df4cdf253968f5858687ffac315e22cf0e4 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 13 Dec 2024 12:56:24 +0000 Subject: [PATCH 398/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a4ae75914c..bc99b11e06 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.11+dev" +__version__ = "1.0.12" diff --git a/package.py b/package.py index b8d88fc2ad..df9bafba1e 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.11+dev" +version = "1.0.12" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index bdfaf797e4..b35359abdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.11+dev" +version = "1.0.12" description = "" authors = ["Ynput Team "] readme = "README.md" From 704b011474c99a60ef2584de6fd5b59d230422fd Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 13 Dec 2024 12:57:09 +0000 Subject: [PATCH 399/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index bc99b11e06..2417897a47 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.12" +__version__ = "1.0.12+dev" diff --git a/package.py b/package.py index df9bafba1e..8ade5ceeed 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.12" +version = "1.0.12+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index b35359abdb..b8d6a5a537 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.12" +version = "1.0.12+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From b8269f7b3106eefecf7ec30967d7f8bb4260816e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 15 Dec 2024 11:44:57 +0100 Subject: [PATCH 400/546] Always increment workfile when requested - instead of only when no unsaved changes --- client/ayon_core/pipeline/context_tools.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 44c9e5d673..b9ae906ab4 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -585,9 +585,6 @@ def version_up_current_workfile(): """Function to increment and save workfile """ host = registered_host() - if not host.has_unsaved_changes(): - print("No unsaved changes, skipping file save..") - return project_name = get_current_project_name() folder_path = get_current_folder_path() From 145688d56f28aee11ab9eb4e97e40a94a3926841 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 16 Dec 2024 10:27:01 +0100 Subject: [PATCH 401/546] Editorial: Fix clip_media source for review track. --- .../plugins/publish/collect_otio_subset_resources.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 2d8e91fe09..199e952769 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -178,7 +178,8 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, collection=collection) - if "review" in instance.data["families"]: + if ("review" in instance.data["families"] and + not instance.data.get("otioReviewClips")): review_repre = self._create_representation( frame_start, frame_end, collection=collection, delete=True, review=True) @@ -197,7 +198,8 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, file=filename, trim=_trim) - if "review" in instance.data["families"]: + if ("review" in instance.data["families"] and + not instance.data.get("otioReviewClips")): review_repre = self._create_representation( frame_start, frame_end, file=filename, delete=True, review=True) From e7d95c1d5d82a391e311952fc4a3143ad9bd6d77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:29:25 +0100 Subject: [PATCH 402/546] add methods to get launcher action paths --- client/ayon_core/addon/base.py | 15 +++++++++++++++ client/ayon_core/addon/interfaces.py | 7 +++++++ 2 files changed, 22 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index ed6b82ef52..72270fa585 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -894,6 +894,21 @@ class AddonsManager: output.extend(paths) return output + def collect_launcher_action_paths(self): + """Helper to collect launcher action paths from addons. + + Returns: + list: List of paths to launcher actions. + + """ + output = self._collect_plugin_paths( + "get_launcher_action_paths" + ) + # Add default core actions + actions_dir = os.path.join(AYON_CORE_ROOT, "plugins", "actions") + output.insert(0, actions_dir) + return output + def collect_create_plugin_paths(self, host_name): """Helper to collect creator plugin paths from addons. diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index 2616913dc0..72191e3453 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -54,6 +54,13 @@ class IPluginPaths(AYONInterface): paths = [paths] return paths + def get_launcher_action_paths(self): + """Receive launcher actions paths. + + Give addons ability to add launcher actions paths. + """ + return self._get_plugin_paths_by_type("actions") + def get_create_plugin_paths(self, host_name): """Receive create plugin paths. From 397a85de5ab1b1032c558d5fe4c157bbeb90925f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:42:02 +0100 Subject: [PATCH 403/546] fix discovery of actions --- client/ayon_core/tools/launcher/models/actions.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 8bd30daffa..e1612e2b9f 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -7,6 +7,7 @@ from ayon_core.pipeline.actions import ( discover_launcher_actions, LauncherAction, LauncherActionSelection, + register_launcher_action_path, ) from ayon_core.pipeline.workfile import should_use_last_workfile_on_launch @@ -459,6 +460,14 @@ class ActionsModel: def _get_discovered_action_classes(self): if self._discovered_actions is None: + # NOTE We don't need to register the paths, but that would + # require to change discovery logic and deprecate all functions + # related to registering and discovering launcher actions. + addons_manager = self._get_addons_manager() + actions_paths = addons_manager.collect_launcher_action_paths() + for path in actions_paths: + if path and os.path.exists(path): + register_launcher_action_path(path) self._discovered_actions = ( discover_launcher_actions() + self._get_applications_action_classes() From 8b663ef4400fe99736da40b59a707ecf492f5437 Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Mon, 16 Dec 2024 11:07:21 +0100 Subject: [PATCH 404/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../plugins/publish/collect_otio_subset_resources.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 199e952769..0fb30326c6 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -178,8 +178,10 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, collection=collection) - if ("review" in instance.data["families"] and - not instance.data.get("otioReviewClips")): + if ( + not instance.data.get("otioReviewClips") + and "review" in instance.data["families"] + ): review_repre = self._create_representation( frame_start, frame_end, collection=collection, delete=True, review=True) @@ -198,8 +200,10 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, file=filename, trim=_trim) - if ("review" in instance.data["families"] and - not instance.data.get("otioReviewClips")): + if ( + not instance.data.get("otioReviewClips") + and "review" in instance.data["families"] + ): review_repre = self._create_representation( frame_start, frame_end, file=filename, delete=True, review=True) From 699da55d53cf0d48046f854062057f3797b2ca78 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:57:25 +0100 Subject: [PATCH 405/546] refresh actions when on projects page --- client/ayon_core/tools/launcher/ui/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/window.py b/client/ayon_core/tools/launcher/ui/window.py index 34aeab35bb..2d52a73c38 100644 --- a/client/ayon_core/tools/launcher/ui/window.py +++ b/client/ayon_core/tools/launcher/ui/window.py @@ -202,8 +202,9 @@ class LauncherWindow(QtWidgets.QWidget): self._go_to_hierarchy_page(project_name) def _on_projects_refresh(self): - # There is nothing to do, we're on projects page + # Refresh only actions on projects page if self._is_on_projects_page: + self._actions_widget.refresh() return # No projects were found -> go back to projects page From ea292add98bae40e45388949207290bcc992788a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 16 Dec 2024 22:56:27 +0100 Subject: [PATCH 406/546] Use underscore separator like in Maya settings `maya_dirmap`. Only other integration I can see that has dirmapping is Nuke, which uses just `dirmap` without host prefix - which I suppose would then be broken regardless. It may make more sense to remove the `host` specific prefix from the label because it's already looking in host specific settings anyway. --- client/ayon_core/host/dirmap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/host/dirmap.py b/client/ayon_core/host/dirmap.py index 19841845e7..c932c13c10 100644 --- a/client/ayon_core/host/dirmap.py +++ b/client/ayon_core/host/dirmap.py @@ -118,7 +118,7 @@ class HostDirmap(ABC): site, in that case configuration in Local Settings takes precedence """ - dirmap_label = "{}-dirmap".format(self.host_name) + dirmap_label = "{}_dirmap".format(self.host_name) mapping_sett = self.project_settings[self.host_name].get(dirmap_label, {}) local_mapping = self._get_local_sync_dirmap() From 5e503d0b51f3f587f369f877c52a304e907e7bec Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 17 Dec 2024 10:46:47 +0100 Subject: [PATCH 407/546] Remove host name prefix from dirmap settings mapping --- client/ayon_core/host/dirmap.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/client/ayon_core/host/dirmap.py b/client/ayon_core/host/dirmap.py index c932c13c10..3f02be6614 100644 --- a/client/ayon_core/host/dirmap.py +++ b/client/ayon_core/host/dirmap.py @@ -117,10 +117,7 @@ class HostDirmap(ABC): It checks if Site Sync is enabled and user chose to use local site, in that case configuration in Local Settings takes precedence """ - - dirmap_label = "{}_dirmap".format(self.host_name) - mapping_sett = self.project_settings[self.host_name].get(dirmap_label, - {}) + mapping_sett = self.project_settings[self.host_name].get("dirmap", {}) local_mapping = self._get_local_sync_dirmap() mapping_enabled = mapping_sett.get("enabled") or bool(local_mapping) if not mapping_enabled: From 4c33041de1fa83cf320640147b14848258190d87 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 17 Dec 2024 11:13:14 +0100 Subject: [PATCH 408/546] Fix broken editorial tests. --- .../editorial/test_extract_otio_review.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 8b1c9da30e..e1fbf514d4 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -103,17 +103,17 @@ def test_image_sequence_with_embedded_tc_and_handles_out_of_range(): # 10 head black handles generated from gap (991-1000) "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " "color=c=black:s=1280x720 -tune stillimage -start_number 991 " - "C:/result/output.%03d.jpg", + "C:/result/output.%04d.jpg", # 10 tail black handles generated from gap (1102-1111) "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " "color=c=black:s=1280x720 -tune stillimage -start_number 1102 " - "C:/result/output.%03d.jpg", + "C:/result/output.%04d.jpg", # Report from source exr (1001-1101) with enforce framerate "/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i " f"C:\\exr_embedded_tc{os.sep}output.%04d.exr -start_number 1001 " - "C:/result/output.%03d.jpg" + "C:/result/output.%04d.jpg" ] assert calls == expected @@ -131,11 +131,11 @@ def test_image_sequence_and_handles_out_of_range(): expected = [ # 5 head black frames generated from gap (991-995) "/path/to/ffmpeg -t 0.2 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 991 C:/result/output.%03d.jpg", + " -tune stillimage -start_number 991 C:/result/output.%04d.jpg", # 9 tail back frames generated from gap (1097-1105) "/path/to/ffmpeg -t 0.36 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 1097 C:/result/output.%03d.jpg", + " -tune stillimage -start_number 1097 C:/result/output.%04d.jpg", # Report from source tiff (996-1096) # 996-1000 = additional 5 head frames @@ -143,7 +143,7 @@ def test_image_sequence_and_handles_out_of_range(): # 1096-1096 = additional 1 tail frames "/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i " f"C:\\tif_seq{os.sep}output.%04d.tif -start_number 996" - f" C:/result/output.%03d.jpg" + f" C:/result/output.%04d.jpg" ] assert calls == expected @@ -164,7 +164,7 @@ def test_movie_with_embedded_tc_no_gap_handles(): # - duration = 68fr (source) + 20fr (handles) = 88frames = 3.666s "/path/to/ffmpeg -ss 0.16666666666666666 -t 3.6666666666666665 " "-i C:\\data\\qt_embedded_tc.mov -start_number 991 " - "C:/result/output.%03d.jpg" + "C:/result/output.%04d.jpg" ] assert calls == expected @@ -181,12 +181,12 @@ def test_short_movie_head_gap_handles(): expected = [ # 10 head black frames generated from gap (991-1000) "/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi -i color=c=black:s=1280x720" - " -tune stillimage -start_number 991 C:/result/output.%03d.jpg", + " -tune stillimage -start_number 991 C:/result/output.%04d.jpg", # source range + 10 tail frames # duration = 50fr (source) + 10fr (tail handle) = 60 fr = 2.4s "/path/to/ffmpeg -ss 0.0 -t 2.4 -i C:\\data\\movie.mp4" - " -start_number 1001 C:/result/output.%03d.jpg" + " -start_number 1001 C:/result/output.%04d.jpg" ] assert calls == expected @@ -204,13 +204,13 @@ def test_short_movie_tail_gap_handles(): # 10 tail black frames generated from gap (1067-1076) "/path/to/ffmpeg -t 0.4166666666666667 -r 24.0 -f lavfi -i " "color=c=black:s=1280x720 -tune stillimage -start_number 1067 " - "C:/result/output.%03d.jpg", + "C:/result/output.%04d.jpg", # 10 head frames + source range # duration = 10fr (head handle) + 66fr (source) = 76fr = 3.16s "/path/to/ffmpeg -ss 1.0416666666666667 -t 3.1666666666666665 -i " "C:\\data\\qt_no_tc_24fps.mov -start_number 991" - " C:/result/output.%03d.jpg" + " C:/result/output.%04d.jpg" ] assert calls == expected @@ -238,62 +238,62 @@ def test_multiple_review_clips_no_gap(): # 10 head black frames generated from gap (991-1000) '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi' ' -i color=c=black:s=1280x720 -tune ' - 'stillimage -start_number 991 C:/result/output.%03d.jpg', + 'stillimage -start_number 991 C:/result/output.%04d.jpg', # Alternance 25fps tiff sequence and 24fps exr sequence # for 100 frames each '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1001 C:/result/output.%03d.jpg', + '-start_number 1001 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1102 C:/result/output.%03d.jpg', + '-start_number 1102 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1199 C:/result/output.%03d.jpg', + '-start_number 1199 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1300 C:/result/output.%03d.jpg', + '-start_number 1300 C:/result/output.%04d.jpg', # Repeated 25fps tiff sequence multiple times till the end '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1397 C:/result/output.%03d.jpg', + '-start_number 1397 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1498 C:/result/output.%03d.jpg', + '-start_number 1498 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1599 C:/result/output.%03d.jpg', + '-start_number 1599 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1700 C:/result/output.%03d.jpg', + '-start_number 1700 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1801 C:/result/output.%03d.jpg', + '-start_number 1801 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1902 C:/result/output.%03d.jpg', + '-start_number 1902 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2003 C:/result/output.%03d.jpg', + '-start_number 2003 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2104 C:/result/output.%03d.jpg', + '-start_number 2104 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2205 C:/result/output.%03d.jpg' + '-start_number 2205 C:/result/output.%04d.jpg' ] assert calls == expected @@ -321,15 +321,15 @@ def test_multiple_review_clips_with_gap(): # Gap on review track (12 frames) '/path/to/ffmpeg -t 0.5 -r 24.0 -f lavfi' ' -i color=c=black:s=1280x720 -tune ' - 'stillimage -start_number 991 C:/result/output.%03d.jpg', + 'stillimage -start_number 991 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1003 C:/result/output.%03d.jpg', + '-start_number 1003 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1091 C:/result/output.%03d.jpg' + '-start_number 1091 C:/result/output.%04d.jpg' ] assert calls == expected From 5780a1797115554ebff370bd4634420ddba4fc0f Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 18 Dec 2024 18:07:16 +0100 Subject: [PATCH 409/546] Consolidate 23.976 trim computation. --- client/ayon_core/pipeline/editorial.py | 81 +++++--- .../plugins/publish/extract_otio_review.py | 6 +- .../resources/qt_23.976_embedded_long_tc.json | 174 ++++++++++++++++++ .../editorial/test_extract_otio_review.py | 22 +-- .../test_media_range_with_retimes.py | 22 +++ 5 files changed, 262 insertions(+), 43 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_23.976_embedded_long_tc.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 2928ef5f63..d71cf6c344 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -196,11 +196,11 @@ def is_clip_from_media_sequence(otio_clip): return is_input_sequence or is_input_sequence_legacy -def remap_range_on_file_sequence(otio_clip, in_out_range): +def remap_range_on_file_sequence(otio_clip, otio_range): """ Args: otio_clip (otio.schema.Clip): The OTIO clip to check. - in_out_range (tuple[float, float]): The in-out range to remap. + otio_range (otio.schema.TimeRange): The trim range to apply. Returns: tuple(int, int): The remapped range as discrete frame number. @@ -211,17 +211,25 @@ def remap_range_on_file_sequence(otio_clip, in_out_range): if not is_clip_from_media_sequence(otio_clip): raise ValueError(f"Cannot map on non-file sequence clip {otio_clip}.") - try: - media_in_trimmed, media_out_trimmed = in_out_range - - except ValueError as error: - raise ValueError("Invalid in_out_range provided.") from error - media_ref = otio_clip.media_reference available_range = otio_clip.available_range() - source_range = otio_clip.source_range available_range_rate = available_range.start_time.rate - media_in = available_range.start_time.value + + # Backward-compatibility for Hiero OTIO exporter. + # NTSC compatibility might introduce floating rates, when these are + # not exactly the same (23.976 vs 23.976024627685547) + # this will cause precision issue in computation. + # Currently round to 2 decimals for comparison, + # but this should always rescale after that. + rounded_av_rate = round(available_range_rate, 2) + rounded_range_rate = round(otio_range.start_time.rate, 2) + + if rounded_av_rate != rounded_range_rate: + raise ValueError("Inconsistent range between clip and provided clip") + + source_range = otio_clip.source_range + source_range_rate = source_range.start_time.rate + media_in = available_range.start_time available_range_start_frame = ( available_range.start_time.to_frames() ) @@ -236,14 +244,20 @@ def remap_range_on_file_sequence(otio_clip, in_out_range): and available_range_start_frame == media_ref.start_frame and source_range.start_time.to_frames() < media_ref.start_frame ): - media_in = 0 + media_in = otio.opentime.RationalTime( + 0, rate=available_range_rate + ) + src_offset_in = otio_range.start_time - media_in frame_in = otio.opentime.RationalTime.from_frames( - media_in_trimmed - media_in + media_ref.start_frame, + media_ref.start_frame + src_offset_in.to_frames(), rate=available_range_rate, ).to_frames() + + range_duration = otio_range.duration + frame_out = otio.opentime.RationalTime.from_frames( - media_out_trimmed - media_in + media_ref.start_frame, + frame_in + otio_range.duration.to_frames() - 1, rate=available_range_rate, ).to_frames() @@ -374,31 +388,44 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): offset_in, offset_out = offset_out, offset_in handle_start, handle_end = handle_end, handle_start - # compute retimed range - media_in_trimmed = conformed_source_range.start_time.value + offset_in - media_out_trimmed = media_in_trimmed + ( - ( - conformed_source_range.duration.value - * abs(time_scalar) - + offset_out - ) - 1 - ) - - media_in = available_range.start_time.value - media_out = available_range.end_time_inclusive().value - # If media source is an image sequence, returned # mediaIn/mediaOut have to correspond # to frame numbers from source sequence. if is_input_sequence: + + src_in = conformed_source_range.start_time + src_duration = conformed_source_range.duration + + offset_in = otio.opentime.RationalTime(offset_in, rate=src_in.rate) + offset_duration = otio.opentime.RationalTime(offset_out, rate=src_duration.rate) + + trim_range = otio.opentime.TimeRange( + start_time=src_in + offset_in, + duration=src_duration + offset_duration + ) + # preserve discrete frame numbers media_in_trimmed, media_out_trimmed = remap_range_on_file_sequence( otio_clip, - (media_in_trimmed, media_out_trimmed) + trim_range, ) media_in = media_ref.start_frame media_out = media_in + available_range.duration.to_frames() - 1 + else: + # compute retimed range + media_in_trimmed = conformed_source_range.start_time.value + offset_in + media_out_trimmed = media_in_trimmed + ( + ( + conformed_source_range.duration.value + * abs(time_scalar) + + offset_out + ) - 1 + ) + + media_in = available_range.start_time.value + media_out = available_range.end_time_inclusive().value + # adjust available handles if needed if (media_in_trimmed - media_in) < handle_start: handle_start = max(0, media_in_trimmed - media_in) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 712ae7a886..d5f5f43cc9 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -209,13 +209,9 @@ class ExtractOTIOReview( # File sequence way if is_sequence: # Remap processing range to input file sequence. - processing_range_as_frames = ( - processing_range.start_time.to_frames(), - processing_range.end_time_inclusive().to_frames() - ) first, last = remap_range_on_file_sequence( r_otio_cl, - processing_range_as_frames, + processing_range, ) input_fps = processing_range.start_time.rate diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_23.976_embedded_long_tc.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_23.976_embedded_long_tc.json new file mode 100644 index 0000000000..01d81508d1 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_23.976_embedded_long_tc.json @@ -0,0 +1,174 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "Main088sh110", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 82.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 1937905.9905694576 + } + }, + "effects": [], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/088/Main088sh110\", \"task\": null, \"clip_index\": \"70C9FA86-76A5-A045-A004-3158FB3F27C5\", \"hierarchy\": \"shots/088\", \"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\", \"shot\": \"sh110\", \"reviewableSource\": \"Reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1009, \"handleStart\": 8, \"handleEnd\": 8, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"088\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\"}, \"heroTrack\": true, \"uuid\": \"8b0d1db8-7094-48ba-b2cd-df0d43cfffda\", \"reviewTrack\": \"Reference\", \"review\": true, \"folderName\": \"Main088sh110\", \"label\": \"/shots/088/Main088sh110 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"f6b7f12c-f3a8-44fd-b4e4-acc63ed80bb1\", \"creator_attributes\": {\"workfileFrameStart\": 1009, \"handleStart\": 8, \"handleEnd\": 8, \"frameStart\": 1009, \"frameEnd\": 1091, \"clipIn\": 80, \"clipOut\": 161, \"clipDuration\": 82, \"sourceIn\": 8.0, \"sourceOut\": 89.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Main\", \"folderPath\": \"/shots/088/Main088sh110\", \"task\": null, \"clip_index\": \"70C9FA86-76A5-A045-A004-3158FB3F27C5\", \"hierarchy\": \"shots/088\", \"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\", \"shot\": \"sh110\", \"reviewableSource\": \"Reference\", \"sourceResolution\": false, \"workfileFrameStart\": 1009, \"handleStart\": 8, \"handleEnd\": 8, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"088\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"404\", \"sequence\": \"088\", \"track\": \"Main\"}, \"heroTrack\": true, \"uuid\": \"8b0d1db8-7094-48ba-b2cd-df0d43cfffda\", \"reviewTrack\": \"Reference\", \"review\": true, \"folderName\": \"Main088sh110\", \"parent_instance_id\": \"f6b7f12c-f3a8-44fd-b4e4-acc63ed80bb1\", \"label\": \"/shots/088/Main088sh110 plateMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"64b54c11-7ab1-45ef-b156-9ed5d5552b9b\", \"creator_attributes\": {\"parentInstance\": \"/shots/088/Main088sh110 shotMain\", \"review\": true, \"reviewableSource\": \"Reference\"}, \"publish_attributes\": {}}}, \"clip_index\": \"70C9FA86-76A5-A045-A004-3158FB3F27C5\"}", + "label": "AYONdata_6b797112", + "note": "AYON data container" + }, + "name": "AYONdata_6b797112", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "Input - Sony - Linear - Venice S-Gamut3.Cine", + "ayon.source.height": 2160, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 4096, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "Input - Sony - Linear - Venice S-Gamut3.Cine", + "foundry.source.duration": "98", + "foundry.source.filename": "409_083_0015.%04d.exr 1001-1098", + "foundry.source.filesize": "", + "foundry.source.fragments": "98", + "foundry.source.framerate": "23.98", + "foundry.source.fullpath": "", + "foundry.source.height": "2160", + "foundry.source.layers": "colour", + "foundry.source.path": "X:/prj/AYON_CIRCUIT_TEST/data/OBX_20240729_P159_DOG_409/EXR/409_083_0015/409_083_0015.%04d.exr 1001-1098", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 368", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "409_083_0015.%04d.exr 1001-1098", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "1001", + "foundry.source.timecode": "1937896", + "foundry.source.umid": "4b3e13b3-e465-4df4-cb1f-257091b63815", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "4096", + "foundry.timeline.colorSpace": "Input - Sony - Linear - Venice S-Gamut3.Cine", + "foundry.timeline.duration": "98", + "foundry.timeline.framerate": "23.98", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAABqAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.camera_camera_type": "AXS-R7", + "media.exr.camera_fps": "23.976", + "media.exr.camera_id": "MPC-3610 0010762 Version6.30", + "media.exr.camera_iso": "2500", + "media.exr.camera_lens_type": "Unknown", + "media.exr.camera_monitor_space": "OBX4_LUT_1_Night.cube", + "media.exr.camera_nd_filter": "1", + "media.exr.camera_roll_angle": "0.3", + "media.exr.camera_shutter_angle": "180.0", + "media.exr.camera_shutter_speed": "0.0208333", + "media.exr.camera_shutter_type": "Speed and Angle", + "media.exr.camera_sl_num": "00011434", + "media.exr.camera_tilt_angle": "-7.4", + "media.exr.camera_type": "Sony", + "media.exr.camera_white_kelvin": "3200", + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.clip_details_codec": "F55_X-OCN_ST_4096_2160", + "media.exr.clip_details_pixel_aspect_ratio": "1", + "media.exr.clip_details_shot_frame_rate": "23.98p", + "media.exr.compression": "0", + "media.exr.compressionName": "none", + "media.exr.dataWindow": "0,0,4095,2159", + "media.exr.displayWindow": "0,0,4095,2159", + "media.exr.lineOrder": "0", + "media.exr.owner": "C272C010_240530HO", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.tech_details_aspect_ratio": "1.8963", + "media.exr.tech_details_cdl_sat": "1", + "media.exr.tech_details_cdl_sop": "(1 1 1)(0 0 0)(1 1 1)", + "media.exr.tech_details_gamma_space": "R709 Video", + "media.exr.tech_details_par": "1", + "media.exr.type": "scanlineimage", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2024-07-30 18:51:38", + "media.input.filename": "X:/prj/AYON_CIRCUIT_TEST/data/OBX_20240729_P159_DOG_409/EXR/409_083_0015/409_083_0015.1001.exr", + "media.input.filereader": "exr", + "media.input.filesize": "53120020", + "media.input.frame": "1", + "media.input.frame_rate": "23.976", + "media.input.height": "2160", + "media.input.mtime": "2024-07-30 18:51:38", + "media.input.timecode": "22:25:45:16", + "media.input.width": "4096", + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 98.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 1937896.0 + } + }, + "available_image_bounds": null, + "target_url_base": "X:/prj/AYON_CIRCUIT_TEST/data/OBX_20240729_P159_DOG_409/EXR/409_083_0015\\", + "name_prefix": "409_083_0015.", + "name_suffix": ".exr", + "start_frame": 1001, + "frame_step": 1, + "rate": 23.976, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index e1fbf514d4..8ad2e44b06 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -252,48 +252,48 @@ def test_multiple_review_clips_no_gap(): '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1199 C:/result/output.%04d.jpg', + '-start_number 1198 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-start_number 1300 C:/result/output.%04d.jpg', + '-start_number 1299 C:/result/output.%04d.jpg', # Repeated 25fps tiff sequence multiple times till the end '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1397 C:/result/output.%04d.jpg', + '-start_number 1395 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1498 C:/result/output.%04d.jpg', + '-start_number 1496 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1599 C:/result/output.%04d.jpg', + '-start_number 1597 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1700 C:/result/output.%04d.jpg', + '-start_number 1698 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1801 C:/result/output.%04d.jpg', + '-start_number 1799 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 1902 C:/result/output.%04d.jpg', + '-start_number 1900 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2003 C:/result/output.%04d.jpg', + '-start_number 2001 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2104 C:/result/output.%04d.jpg', + '-start_number 2102 C:/result/output.%04d.jpg', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-start_number 2205 C:/result/output.%04d.jpg' + '-start_number 2203 C:/result/output.%04d.jpg' ] assert calls == expected diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 7f9256c6d8..5a375e4499 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -64,6 +64,28 @@ def test_movie_embedded_tc_handle(): ) +def test_movie_23fps_qt_embedded_tc(): + """ + Movie clip (embedded timecode 1h) + available_range = 1937896-1937994 23.976fps + source_range = 1937905-1937987 23.97602462768554fps + """ + expected_data = { + 'mediaIn': 1009, + 'mediaOut': 1090, + 'handleStart': 8, + 'handleEnd': 8, + 'speed': 1.0 + } + + _check_expected_retimed_values( + "qt_23.976_embedded_long_tc.json", + expected_data, + handle_start=8, + handle_end=8, + ) + + def test_movie_retime_effect(): """ Movie clip (embedded timecode 1h) From 037db5dbd31ab615f80600d5926bb2ec901f0bf5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 Dec 2024 12:23:43 +0100 Subject: [PATCH 410/546] Store in instance data whether the staging dir set is a custom one --- client/ayon_core/pipeline/publish/lib.py | 1 + client/ayon_core/pipeline/staging_dir.py | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index ecdcc0f0c1..586b90a3fd 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -717,6 +717,7 @@ def get_instance_staging_dir(instance): instance.data.update({ "stagingDir": staging_dir_path, "stagingDir_persistent": staging_dir_info.persistent, + "stagingDir_custom": staging_dir_info.custom }) return staging_dir_path diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index ea22d99389..83878f17a2 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -12,6 +12,7 @@ from .tempdir import get_temp_dir class StagingDir: directory: str persistent: bool + custom: bool # Whether the staging dir is a custom staging dir def get_staging_dir_config( @@ -204,7 +205,8 @@ def get_staging_dir_info( dir_template = staging_dir_config["template"]["directory"] return StagingDir( dir_template.format_strict(ctx_data), - staging_dir_config["persistence"], + persistent=staging_dir_config["persistence"], + custom=True ) # no config found but force an output @@ -216,7 +218,8 @@ def get_staging_dir_info( prefix=prefix, suffix=suffix, ), - False, + persistent=False, + custom=False ) return None From 58d3852f2893e87cc16f47e81112232c2282f4e4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 7 Jan 2025 06:27:26 +0100 Subject: [PATCH 411/546] Fix red dot for FORCE_NOT_OPEN_WORKFILE_ROLE to be drawn on wrong location if app is not on first row --- client/ayon_core/tools/launcher/ui/actions_widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/ui/actions_widget.py b/client/ayon_core/tools/launcher/ui/actions_widget.py index 2ffce13292..c64d718172 100644 --- a/client/ayon_core/tools/launcher/ui/actions_widget.py +++ b/client/ayon_core/tools/launcher/ui/actions_widget.py @@ -265,7 +265,7 @@ class ActionDelegate(QtWidgets.QStyledItemDelegate): if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): rect = QtCore.QRectF( - option.rect.x(), option.rect.height(), 5, 5) + option.rect.x(), option.rect.y() + option.rect.height(), 5, 5) painter.setPen(QtCore.Qt.NoPen) painter.setBrush(QtGui.QColor(200, 0, 0)) painter.drawEllipse(rect) From c6ea24edcfc3d110c26a5c0084136035d3f1745a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:37:03 +0100 Subject: [PATCH 412/546] return StagingDir object instead of string --- client/ayon_core/pipeline/staging_dir.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 83878f17a2..b7ca1a2cd6 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -161,11 +161,15 @@ def get_staging_dir_info( ) if force_tmp_dir: - return get_temp_dir( - project_name=project_entity["name"], - anatomy=anatomy, - prefix=prefix, - suffix=suffix, + return StagingDir( + get_temp_dir( + project_name=project_entity["name"], + anatomy=anatomy, + prefix=prefix, + suffix=suffix, + ), + is_persistent=False, + is_custom=False ) # making few queries to database From 228a3c6f054388bad49dfeb020615e4b3eb2fdb8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:37:33 +0100 Subject: [PATCH 413/546] remove duplicated validation of template name --- client/ayon_core/pipeline/staging_dir.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index b7ca1a2cd6..7f9ec85466 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -76,7 +76,6 @@ def get_staging_dir_config( # get template from template name template_name = profile["template_name"] - _validate_template_name(project_name, template_name, anatomy) template = anatomy.get_template_item("staging", template_name) @@ -93,19 +92,6 @@ def get_staging_dir_config( return {"template": template, "persistence": data_persistence} -def _validate_template_name(project_name, template_name, anatomy): - """Check that staging dir section with appropriate template exist. - - Raises: - ValueError - if misconfigured template - """ - if template_name not in anatomy.templates["staging"]: - raise ValueError( - f'Anatomy of project "{project_name}" does not have set' - f' "{template_name}" template key at Staging Dir category!' - ) - - def get_staging_dir_info( project_entity, folder_entity, From 2b1a04b5c0742320f1eb5adc501aa610e3d5e11c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:38:44 +0100 Subject: [PATCH 414/546] added typehints --- .../pipeline/create/creator_plugins.py | 4 +- client/ayon_core/pipeline/staging_dir.py | 53 ++++++++++--------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 6ccafe1bc7..28e9de20ee 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -15,7 +15,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path ) -from ayon_core.pipeline import get_staging_dir_info +from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -833,7 +833,7 @@ class Creator(BaseCreator): """ return self.pre_create_attr_defs - def get_staging_dir(self, instance): + def get_staging_dir(self, instance) -> Optional[StagingDir]: """Return the staging dir and persistence from instance. Args: diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 7f9ec85466..7e0874fbef 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -1,3 +1,6 @@ +import logging +import warnings +from typing import Optional, Dict, Any from dataclasses import dataclass from ayon_core.lib import Logger, filter_profiles @@ -16,16 +19,16 @@ class StagingDir: def get_staging_dir_config( - project_name, - task_type, - task_name, - product_type, - product_name, - host_name, - project_settings=None, - anatomy=None, - log=None, -): + project_name: str, + task_type: Optional[str, None], + task_name: Optional[str, None], + product_type: str, + product_name: str, + host_name: str, + project_settings: Optional[Dict[str, Any]] = None, + anatomy: Optional[Anatomy] = None, + log: Optional[logging.Logger] = None, +) -> Optional[Dict[str, Any]]: """Get matching staging dir profile. Args: @@ -93,21 +96,21 @@ def get_staging_dir_config( def get_staging_dir_info( - project_entity, - folder_entity, - task_entity, - product_type, - product_name, - host_name, - anatomy=None, - project_settings=None, - template_data=None, - always_return_path=True, - force_tmp_dir=False, - logger=None, - prefix=None, - suffix=None, -): + project_entity: Dict[str, Any], + folder_entity: Optional[Dict[str, Any]], + task_entity: Optional[Dict[str, Any]], + product_type: str, + product_name: str, + host_name: str, + anatomy: Optional[Anatomy] = None, + project_settings: Optional[Dict[str, Any]] = None, + template_data: Optional[Dict[str, Any]] = None, + always_return_path: bool = True, + force_tmp_dir: bool = False, + logger: Optional[logging.Logger] = None, + prefix: Optional[str] = None, + suffix: Optional[str] = None, +) -> Optional[StagingDir]: """Get staging dir info data. If `force_temp` is set, staging dir will be created as tempdir. From 25f6ec241bbbbc3fb95e7770561a96e875341b1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:39:57 +0100 Subject: [PATCH 415/546] added is_ prefix to StagingDir bools --- .../pipeline/create/creator_plugins.py | 2 +- client/ayon_core/pipeline/publish/lib.py | 4 +-- client/ayon_core/pipeline/staging_dir.py | 32 +++++++++++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 28e9de20ee..42e8e0b60f 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -915,7 +915,7 @@ class Creator(BaseCreator): instance.transient_data.update({ "stagingDir": staging_dir_path, - "stagingDir_persistent": staging_dir_info.persistent, + "stagingDir_persistent": staging_dir_info.is_persistent, }) self.log.info(f"Applied staging dir to instance: {staging_dir_path}") diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 586b90a3fd..ba0f846fe4 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -716,8 +716,8 @@ def get_instance_staging_dir(instance): os.makedirs(staging_dir_path, exist_ok=True) instance.data.update({ "stagingDir": staging_dir_path, - "stagingDir_persistent": staging_dir_info.persistent, - "stagingDir_custom": staging_dir_info.custom + "stagingDir_persistent": staging_dir_info.is_persistent, + "stagingDir_custom": staging_dir_info.is_custom }) return staging_dir_path diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 7e0874fbef..2d94616faf 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -14,8 +14,28 @@ from .tempdir import get_temp_dir @dataclass class StagingDir: directory: str - persistent: bool - custom: bool # Whether the staging dir is a custom staging dir + is_persistent: bool + # Whether the staging dir is a custom staging dir + is_custom: bool + + def __setattr__(self, key, value): + if key == "persistent": + warnings.warn( + "'StagingDir.persistent' is deprecated." + " Use 'StagingDir.is_persistent' instead.", + DeprecationWarning + ) + key = "is_persistent" + super().__setattr__(key, value) + + @property + def persistent(self): + warnings.warn( + "'StagingDir.persistent' is deprecated." + " Use 'StagingDir.is_persistent' instead.", + DeprecationWarning + ) + return self.is_persistent def get_staging_dir_config( @@ -198,8 +218,8 @@ def get_staging_dir_info( dir_template = staging_dir_config["template"]["directory"] return StagingDir( dir_template.format_strict(ctx_data), - persistent=staging_dir_config["persistence"], - custom=True + is_persistent=staging_dir_config["persistence"], + is_custom=True ) # no config found but force an output @@ -211,8 +231,8 @@ def get_staging_dir_info( prefix=prefix, suffix=suffix, ), - persistent=False, - custom=False + is_persistent=False, + is_custom=False ) return None From 47fee3f54bd7d6b18849cdb59f8b5937988efa57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 7 Jan 2025 15:41:58 +0100 Subject: [PATCH 416/546] change custom key to is_custom --- client/ayon_core/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index ba0f846fe4..40a9b47aba 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -717,7 +717,7 @@ def get_instance_staging_dir(instance): instance.data.update({ "stagingDir": staging_dir_path, "stagingDir_persistent": staging_dir_info.is_persistent, - "stagingDir_custom": staging_dir_info.is_custom + "stagingDir_is_custom": staging_dir_info.is_custom }) return staging_dir_path From 4641760bd17a232a0d6c7a24482060a46a475ba3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 8 Jan 2025 09:16:21 +0100 Subject: [PATCH 417/546] Initial changes for BorisFX Silhouette --- client/ayon_core/hooks/pre_add_last_workfile_arg.py | 3 ++- client/ayon_core/hooks/pre_ocio_hook.py | 3 ++- client/ayon_core/plugins/publish/validate_file_saved.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index d5914c2352..a931fb0cbe 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -29,7 +29,8 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "aftereffects", "wrap", "openrv", - "cinema4d" + "cinema4d", + "silhouette" } launch_types = {LaunchTypes.local} diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 7406aa42cf..dd81cf053e 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -20,7 +20,8 @@ class OCIOEnvHook(PreLaunchHook): "hiero", "resolve", "openrv", - "cinema4d" + "cinema4d", + "silhouette" } launch_types = set() diff --git a/client/ayon_core/plugins/publish/validate_file_saved.py b/client/ayon_core/plugins/publish/validate_file_saved.py index f52998cef3..4f9e84aee0 100644 --- a/client/ayon_core/plugins/publish/validate_file_saved.py +++ b/client/ayon_core/plugins/publish/validate_file_saved.py @@ -37,7 +37,7 @@ class ValidateCurrentSaveFile(pyblish.api.ContextPlugin): label = "Validate File Saved" order = pyblish.api.ValidatorOrder - 0.1 hosts = ["fusion", "houdini", "max", "maya", "nuke", "substancepainter", - "cinema4d"] + "cinema4d", "silhouette"] actions = [SaveByVersionUpAction, ShowWorkfilesAction] def process(self, context): From be6aac6a72a76bb82300fe4ca612e5408bbea679 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 8 Jan 2025 09:17:21 +0100 Subject: [PATCH 418/546] Remove `TreeViewSpinner` with `QtSvg` dependency - The `TreeViewSpinner` widget was not used anywhere - The `QtSvg` dependency does not exist in BorisFX Silhouette so removing it was easiest to make Silhouette not error on this import --- client/ayon_core/tools/utils/views.py | 45 +-------------------------- 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/client/ayon_core/tools/utils/views.py b/client/ayon_core/tools/utils/views.py index b501f1ff11..d8ae94bf0c 100644 --- a/client/ayon_core/tools/utils/views.py +++ b/client/ayon_core/tools/utils/views.py @@ -1,7 +1,6 @@ -from ayon_core.resources import get_image_path from ayon_core.tools.flickcharm import FlickCharm -from qtpy import QtWidgets, QtCore, QtGui, QtSvg +from qtpy import QtWidgets, QtCore, QtGui class DeselectableTreeView(QtWidgets.QTreeView): @@ -19,48 +18,6 @@ class DeselectableTreeView(QtWidgets.QTreeView): QtWidgets.QTreeView.mousePressEvent(self, event) -class TreeViewSpinner(QtWidgets.QTreeView): - size = 160 - - def __init__(self, parent=None): - super(TreeViewSpinner, self).__init__(parent=parent) - - loading_image_path = get_image_path("spinner-200.svg") - - self.spinner = QtSvg.QSvgRenderer(loading_image_path) - - self.is_loading = False - self.is_empty = True - - def paint_loading(self, event): - rect = event.rect() - rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) - rect.moveTo( - rect.x() + rect.width() / 2 - self.size / 2, - rect.y() + rect.height() / 2 - self.size / 2 - ) - rect.setSize(QtCore.QSizeF(self.size, self.size)) - painter = QtGui.QPainter(self.viewport()) - self.spinner.render(painter, rect) - - def paint_empty(self, event): - painter = QtGui.QPainter(self.viewport()) - rect = event.rect() - rect = QtCore.QRectF(rect.topLeft(), rect.bottomRight()) - qtext_opt = QtGui.QTextOption( - QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter - ) - painter.drawText(rect, "No Data", qtext_opt) - - def paintEvent(self, event): - if self.is_loading: - self.paint_loading(event) - elif self.is_empty: - self.paint_empty(event) - else: - super(TreeViewSpinner, self).paintEvent(event) - - class TreeView(QtWidgets.QTreeView): """Ultimate TreeView with flick charm and double click signals. From 2a22bbb0773700f818ab1f1e7dc4c981f5037373 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 8 Jan 2025 11:52:45 +0100 Subject: [PATCH 419/546] Fix Anatomy.format_all with unpadded int values. --- client/ayon_core/lib/path_templates.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index e3cae78a87..057889403c 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -561,9 +561,6 @@ class FormattingPart: """ key = self._template_base - if key in result.really_used_values: - result.add_output(result.really_used_values[key]) - return result # ensure key is properly formed [({})] properly closed. if not self.validate_key_is_matched(key): From cb2758ef54c22da82d172fa54522b9b4bb1fd28e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 8 Jan 2025 13:03:03 +0100 Subject: [PATCH 420/546] Allow folders to be considered as workfiles in workfiles tool, because Silhouette projects are actually folders (with .sfx extension) --- client/ayon_core/tools/workfiles/models/workfiles.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index a268a9bd0e..2c5ec230a6 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -185,8 +185,6 @@ class WorkareaModel: for filename in os.listdir(workdir): filepath = os.path.join(workdir, filename) - if not os.path.isfile(filepath): - continue ext = os.path.splitext(filename)[1].lower() if ext not in self._extensions: From 05ca2d42cd1b3306f81436c2da9d64f4285d2373 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:23:05 +0100 Subject: [PATCH 421/546] fix typehint --- client/ayon_core/pipeline/staging_dir.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 2d94616faf..37d6b955e2 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -40,8 +40,8 @@ class StagingDir: def get_staging_dir_config( project_name: str, - task_type: Optional[str, None], - task_name: Optional[str, None], + task_type: Optional[str], + task_name: Optional[str], product_type: str, product_name: str, host_name: str, From 2c91d60d6df074ebcc74c1eced35fe191e6edeed Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 8 Jan 2025 16:41:06 +0100 Subject: [PATCH 422/546] Fix lint. --- client/ayon_core/pipeline/editorial.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index d71cf6c344..8ac4d906b1 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -228,7 +228,6 @@ def remap_range_on_file_sequence(otio_clip, otio_range): raise ValueError("Inconsistent range between clip and provided clip") source_range = otio_clip.source_range - source_range_rate = source_range.start_time.rate media_in = available_range.start_time available_range_start_frame = ( available_range.start_time.to_frames() @@ -254,8 +253,6 @@ def remap_range_on_file_sequence(otio_clip, otio_range): rate=available_range_rate, ).to_frames() - range_duration = otio_range.duration - frame_out = otio.opentime.RationalTime.from_frames( frame_in + otio_range.duration.to_frames() - 1, rate=available_range_rate, @@ -397,7 +394,10 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): src_duration = conformed_source_range.duration offset_in = otio.opentime.RationalTime(offset_in, rate=src_in.rate) - offset_duration = otio.opentime.RationalTime(offset_out, rate=src_duration.rate) + offset_duration = otio.opentime.RationalTime( + offset_out, + rate=src_duration.rate + ) trim_range = otio.opentime.TimeRange( start_time=src_in + offset_in, From 8db81098af5d15e20607c661ab510c6b6a6ef767 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 8 Jan 2025 17:55:13 +0100 Subject: [PATCH 423/546] Add comment --- client/ayon_core/tools/workfiles/models/workfiles.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 2c5ec230a6..c621a44937 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -184,6 +184,9 @@ class WorkareaModel: return items for filename in os.listdir(workdir): + # We want to support both files and folders. e.g. Silhoutte uses + # folders as its project files. So we do not check whether it is + # a file or not. filepath = os.path.join(workdir, filename) ext = os.path.splitext(filename)[1].lower() From ff2893dff04368f26c127080ef824f3bad95b2d1 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 8 Jan 2025 19:04:54 +0100 Subject: [PATCH 424/546] Fix remap with wrongly detected legacy image sequence. --- client/ayon_core/pipeline/editorial.py | 35 +++++++------ .../img_seq_24_to_23.976_no_legacy.json | 51 +++++++++++++++++++ .../test_media_range_with_retimes.py | 26 ++++++++++ 3 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_24_to_23.976_no_legacy.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 2928ef5f63..1d1859cbb8 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -231,10 +231,13 @@ def remap_range_on_file_sequence(otio_clip, in_out_range): # source range for image sequence. Following code maintain # backward-compatibility by adjusting media_in # while we are updating those. + conformed_src_in = source_range.start_time.rescaled_to( + available_range_rate + ) if ( is_clip_from_media_sequence(otio_clip) and available_range_start_frame == media_ref.start_frame - and source_range.start_time.to_frames() < media_ref.start_frame + and conformed_src_in.to_frames() < media_ref.start_frame ): media_in = 0 @@ -261,21 +264,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_ref = otio_clip.media_reference is_input_sequence = is_clip_from_media_sequence(otio_clip) - # Temporary. - # Some AYON custom OTIO exporter were implemented with relative - # source range for image sequence. Following code maintain - # backward-compatibility by adjusting available range - # while we are updating those. - if ( - is_input_sequence - and available_range.start_time.to_frames() == media_ref.start_frame - and source_range.start_time.to_frames() < media_ref.start_frame - ): - available_range = _ot.TimeRange( - _ot.RationalTime(0, rate=available_range_rate), - available_range.duration, - ) - # Conform source range bounds to available range rate # .e.g. embedded TC of (3600 sec/ 1h), duration 100 frames # @@ -320,6 +308,21 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): else: conformed_source_range = source_range + # Temporary. + # Some AYON custom OTIO exporter were implemented with relative + # source range for image sequence. Following code maintain + # backward-compatibility by adjusting available range + # while we are updating those. + if ( + is_input_sequence + and available_range.start_time.to_frames() == media_ref.start_frame + and conformed_source_range.start_time.to_frames() < media_ref.start_frame + ): + available_range = _ot.TimeRange( + _ot.RationalTime(0, rate=available_range_rate), + available_range.duration, + ) + # modifiers time_scalar = 1. offset_in = 0 diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_24_to_23.976_no_legacy.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_24_to_23.976_no_legacy.json new file mode 100644 index 0000000000..108af0f2c1 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_24_to_23.976_no_legacy.json @@ -0,0 +1,51 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 108.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 883159.0 + } + }, + "effects": [], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 755.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 883750.0 + } + }, + "available_image_bounds": null, + "target_url_base": "/mnt/jobs/yahoo_theDog_1132/IN/FOOTAGE/SCANS_LINEAR/Panasonic Rec 709 to ACESCG/Panasonic P2 /A001_S001_S001_T004/", + "name_prefix": "A001_S001_S001_T004.", + "name_suffix": ".exr", + "start_frame": 883750, + "frame_step": 1, + "rate": 1.0, + "frame_zero_padding": 0, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 7f9256c6d8..331732b6a4 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -187,3 +187,29 @@ def test_img_sequence_conform_to_23_976fps(): handle_start=0, handle_end=8, ) + + +def test_img_sequence_conform_from_24_to_23_976fps(): + """ + Img sequence clip + available files = 883750-884504 24fps + source_range = 883159-883267 23.976fps + + This test ensures such entries do not trigger + the legacy Hiero export compatibility. + """ + expected_data = { + 'mediaIn': 884043, + 'mediaOut': 884150, + 'handleStart': 0, + 'handleEnd': 0, + 'speed': 1.0 + } + + _check_expected_retimed_values( + "img_seq_24_to_23.976_no_legacy.json", + expected_data, + handle_start=0, + handle_end=0, + ) + From 9fbef06cb0c30b1f11e685b9075bf0f53a2064ac Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 8 Jan 2025 19:16:10 +0100 Subject: [PATCH 425/546] Fix lint. --- client/ayon_core/pipeline/editorial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 1d1859cbb8..a33f439651 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -316,7 +316,8 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): if ( is_input_sequence and available_range.start_time.to_frames() == media_ref.start_frame - and conformed_source_range.start_time.to_frames() < media_ref.start_frame + and conformed_source_range.start_time.to_frames() < + media_ref.start_frame ): available_range = _ot.TimeRange( _ot.RationalTime(0, rate=available_range_rate), From cd9929dfa192d080e5dcfea0f543104dd12cb4af Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 9 Jan 2025 17:01:50 +0100 Subject: [PATCH 426/546] set host name environment variable --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index cb52e5c32e..ee88985905 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -24,6 +24,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): # NOTE we should use 'context.data["user"]' but that has higher # order. ("AYON_USERNAME", get_ayon_username()), + ("AYON_HOST_NAME", context.data["hostName"]), ): if value: self.log.debug(f"Setting job env: {key}: {value}") From 3fbb0f4dfb0ecfe46ca83929bbe00a9a92794d34 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:18:24 +0100 Subject: [PATCH 427/546] metadata file does not require 'job' key to be filled --- client/ayon_core/plugins/publish/collect_rendered_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_rendered_files.py b/client/ayon_core/plugins/publish/collect_rendered_files.py index 42ba096d14..62b649cdd0 100644 --- a/client/ayon_core/plugins/publish/collect_rendered_files.py +++ b/client/ayon_core/plugins/publish/collect_rendered_files.py @@ -105,7 +105,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): instance.data.update(instance_data) # stash render job id for later validation - instance.data["render_job_id"] = data.get("job").get("_id") + instance.data["render_job_id"] = data.get("job", {}).get("_id") staging_dir_persistent = instance.data.get( "stagingDir_persistent", False ) From 8a7e11a1a4645bd886154c70eb9ec39b4ee2a831 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:18:40 +0100 Subject: [PATCH 428/546] store metadata file content to 'publishJobMetadata' on instance --- client/ayon_core/plugins/publish/collect_rendered_files.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_rendered_files.py b/client/ayon_core/plugins/publish/collect_rendered_files.py index 62b649cdd0..deecf7ba24 100644 --- a/client/ayon_core/plugins/publish/collect_rendered_files.py +++ b/client/ayon_core/plugins/publish/collect_rendered_files.py @@ -93,8 +93,7 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): # now we can just add instances from json file and we are done any_staging_dir_persistent = False - for instance_data in data.get("instances"): - + for instance_data in data["instances"]: self.log.debug(" - processing instance for {}".format( instance_data.get("productName"))) instance = self._context.create_instance( @@ -105,6 +104,10 @@ class CollectRenderedFiles(pyblish.api.ContextPlugin): instance.data.update(instance_data) # stash render job id for later validation + instance.data["publishJobMetadata"] = data + # TODO remove 'render_job_id' here and rather use + # 'publishJobMetadata' where is needed. + # - this is deadline specific instance.data["render_job_id"] = data.get("job", {}).get("_id") staging_dir_persistent = instance.data.get( "stagingDir_persistent", False From eec3f61641b1ca21b9c6e2fdb225beffcb3c7827 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Jan 2025 15:56:18 +0100 Subject: [PATCH 429/546] Correctly emit signal --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 095a4eae7c..6bd4725371 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -197,7 +197,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget): else: widget = ConvertorItemCardWidget(item, self) widget.selected.connect(self._on_widget_selection) - widget.double_clicked(self.double_clicked) + widget.double_clicked.emit(self.double_clicked) self._widgets_by_id[item.id] = widget self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 From f4ea9aeacff690518822bd76459370fec8596d57 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 13 Jan 2025 16:01:11 +0100 Subject: [PATCH 430/546] Should be ``.connect` --- client/ayon_core/tools/publisher/widgets/card_view_widgets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py index 6bd4725371..2f633b3149 100644 --- a/client/ayon_core/tools/publisher/widgets/card_view_widgets.py +++ b/client/ayon_core/tools/publisher/widgets/card_view_widgets.py @@ -197,7 +197,7 @@ class ConvertorItemsGroupWidget(BaseGroupWidget): else: widget = ConvertorItemCardWidget(item, self) widget.selected.connect(self._on_widget_selection) - widget.double_clicked.emit(self.double_clicked) + widget.double_clicked.connect(self.double_clicked) self._widgets_by_id[item.id] = widget self._content_layout.insertWidget(widget_idx, widget) widget_idx += 1 From 1d23ca6633d5ff8d85ebeda066de7a621a60e869 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 13 Jan 2025 23:35:43 +0200 Subject: [PATCH 431/546] update default order to match values in `CollectUSDLayerContributions` --- server/settings/publish_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 8893b00e23..1bf2e853cf 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1008,8 +1008,8 @@ DEFAULT_PUBLISH_VALUES = { {"name": "model", "order": 100}, {"name": "assembly", "order": 150}, {"name": "groom", "order": 175}, - {"name": "look", "order": 300}, - {"name": "rig", "order": 100}, + {"name": "look", "order": 200}, + {"name": "rig", "order": 300}, # Shot layers {"name": "layout", "order": 200}, {"name": "animation", "order": 300}, From 11b00e61050b3d4027efe7fbedc14111bce87451 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 14 Jan 2025 08:27:36 +0000 Subject: [PATCH 432/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 2417897a47..0a985e2829 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.12+dev" +__version__ = "1.0.13" diff --git a/package.py b/package.py index 8ade5ceeed..dae6a9ca0a 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.12+dev" +version = "1.0.13" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index b8d6a5a537..78edf5c2e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.12+dev" +version = "1.0.13" description = "" authors = ["Ynput Team "] readme = "README.md" From e3b9bfa29d91efc380bde4389783abc0aa9c868d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 14 Jan 2025 08:28:17 +0000 Subject: [PATCH 433/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 0a985e2829..b0ada09e7c 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.13" +__version__ = "1.0.13+dev" diff --git a/package.py b/package.py index dae6a9ca0a..03b69d4c5c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.13" +version = "1.0.13+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 78edf5c2e3..5e42aa7093 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.13" +version = "1.0.13+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From e90ca81eded332464f184025521e8815f6202725 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 14 Jan 2025 10:27:58 +0100 Subject: [PATCH 434/546] Append more editorial tests for reverse speed. --- ...img_seq_reverse_speed_24_to_23.976fps.json | 59 +++ .../img_seq_reverse_speed_no_tc.json | 369 ++++++++++++++++++ .../test_media_range_with_retimes.py | 56 +++ 3 files changed, 484 insertions(+) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_24_to_23.976fps.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_no_tc.json diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_24_to_23.976fps.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_24_to_23.976fps.json new file mode 100644 index 0000000000..b907f53f3d --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_24_to_23.976fps.json @@ -0,0 +1,59 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 32.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976, + "value": 947726.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "", + "effect_name": "LinearTimeWarp", + "time_scalar": -1.0 + } + ], + "markers": [], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 7607.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 941478.0 + } + }, + "available_image_bounds": null, + "target_url_base": "/mnt/jobs/yahoo_theDog_1132/IN/FOOTAGE/SCANS_LINEAR/Panasonic Rec 709 to ACESCG/Panasonic P2 /A001_S001_S001_T012/", + "name_prefix": "A001_S001_S001_T012.", + "name_suffix": ".exr", + "start_frame": 941478, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 0, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_no_tc.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_no_tc.json new file mode 100644 index 0000000000..ccf33413ec --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_no_tc.json @@ -0,0 +1,369 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1000-1099].tif", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 41.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 20.000000000000004 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "", + "effect_name": "", + "time_scalar": -1.0 + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-39": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "961": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-39": { + "Value": 0.8, + "Variant Type": "Double" + }, + "961": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/reverse_speed/sh010\", \"task\": null, \"clip_variant\": \"\", \"clip_index\": \"2cb93726-2f27-41b0-b7f7-f48998327ce8\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"/shots/reverse_speed/sh010\", \"episode\": \"ep01\", \"sequence\": \"reverse_speed\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/reverse_speed\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"reverse_speed\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"reverse_speed\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"d08c8422-29a9-46e9-9d6d-37e2dd9f9f8b\", \"reviewTrack\": null, \"label\": \"/shots/reverse_speed/sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"087e8c66-3ce7-41bf-a27e-3e5f7abc12fb\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1042, \"clipIn\": 86400, \"clipOut\": 86441, \"clipDuration\": 41, \"sourceIn\": 39, \"sourceOut\": 80, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/reverse_speed/sh010\", \"task\": null, \"clip_variant\": \"\", \"clip_index\": \"2cb93726-2f27-41b0-b7f7-f48998327ce8\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"/shots/reverse_speed/sh010\", \"episode\": \"ep01\", \"sequence\": \"reverse_speed\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/reverse_speed\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"reverse_speed\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"reverse_speed\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"d08c8422-29a9-46e9-9d6d-37e2dd9f9f8b\", \"reviewTrack\": null, \"parent_instance_id\": \"087e8c66-3ce7-41bf-a27e-3e5f7abc12fb\", \"label\": \"/shots/reverse_speed/sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"63b97e7f-834f-490d-bba5-ae0e584f4a17\", \"creator_attributes\": {\"parentInstance\": \"/shots/reverse_speed/sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"2cb93726-2f27-41b0-b7f7-f48998327ce8\", \"publish\": true}" + }, + "clip_index": "2cb93726-2f27-41b0-b7f7-f48998327ce8", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "2cb93726-2f27-41b0-b7f7-f48998327ce8", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/reverse_speed/sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "/shots/reverse_speed/sh010", + "folderPath": "/shots/reverse_speed/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/reverse_speed", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "reverse_speed", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "63b97e7f-834f-490d-bba5-ae0e584f4a17", + "label": "/shots/reverse_speed/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "087e8c66-3ce7-41bf-a27e-3e5f7abc12fb", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "reverse_speed", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "reverse_speed", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "d08c8422-29a9-46e9-9d6d-37e2dd9f9f8b", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "2cb93726-2f27-41b0-b7f7-f48998327ce8", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 41, + "clipIn": 86400, + "clipOut": 86441, + "fps": "from_selection", + "frameEnd": 1042, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 39, + "sourceOut": 80, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "/shots/reverse_speed/sh010", + "folderPath": "/shots/reverse_speed/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/reverse_speed", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "reverse_speed", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "087e8c66-3ce7-41bf-a27e-3e5f7abc12fb", + "label": "/shots/reverse_speed/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "reverse_speed", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": null, + "sequence": "reverse_speed", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "d08c8422-29a9-46e9-9d6d-37e2dd9f9f8b", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AYONData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 59.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1000-1099].tif", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 100.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\data\\img_sequence\\tif", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 5147c2394a..2e7193444c 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -235,3 +235,59 @@ def test_img_sequence_conform_from_24_to_23_976fps(): handle_end=0, ) + +def test_img_sequence_reverse_speed_no_tc(): + """ + Img sequence clip + available files = 0-100 24fps + source_range = 20-41 24fps + """ + expected_data = { + 'mediaIn': 1020, + 'mediaOut': 1060, + 'handleStart': 0, + 'handleEnd': 0, + 'speed': -1.0, + 'versionData': { + 'retime': True, + 'speed': -1.0, + 'timewarps': [], + 'handleStart': 0, + 'handleEnd': 0 + } + } + + _check_expected_retimed_values( + "img_seq_reverse_speed_no_tc.json", + expected_data, + handle_start=0, + handle_end=0, + ) + +def test_img_sequence_reverse_speed_from_24_to_23_976fps(): + """ + Img sequence clip + available files = 941478-949084 24fps + source_range = 947726-947757 23.976fps + """ + expected_data = { + 'mediaIn': 948674, + 'mediaOut': 948705, + 'handleStart': 10, + 'handleEnd': 10, + 'speed': -1.0, + 'versionData': { + 'retime': True, + 'speed': -1.0, + 'timewarps': [], + 'handleStart': 10, + 'handleEnd': 10 + } + } + + _check_expected_retimed_values( + "img_seq_reverse_speed_24_to_23.976fps.json", + expected_data, + handle_start=10, + handle_end=10, + ) From 951fd51ab695d2194650237237b73a9170cdc9dc Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 14 Jan 2025 13:17:39 +0100 Subject: [PATCH 435/546] Fix rouding issue with extract_otio_review --- client/ayon_core/plugins/publish/extract_otio_review.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index d5f5f43cc9..275c3e7c58 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -147,7 +147,6 @@ class ExtractOTIOReview( self.actual_fps = available_range.duration.rate start = src_range.start_time.rescaled_to(self.actual_fps) duration = src_range.duration.rescaled_to(self.actual_fps) - src_frame_start = src_range.start_time.to_frames() # Temporary. # Some AYON custom OTIO exporter were implemented with @@ -157,7 +156,7 @@ class ExtractOTIOReview( if ( is_clip_from_media_sequence(r_otio_cl) and available_range_start_frame == media_ref.start_frame - and src_frame_start < media_ref.start_frame + and start.to_frames() < media_ref.start_frame ): available_range = otio.opentime.TimeRange( otio.opentime.RationalTime(0, rate=self.actual_fps), From 3d9459f3c9002cba98652bf05968959deee5cfe8 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 14 Jan 2025 14:17:51 +0100 Subject: [PATCH 436/546] Report stagingDir_is_custom to apply_staging_dir. --- client/ayon_core/pipeline/create/creator_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 42e8e0b60f..445b41cb4b 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -916,6 +916,7 @@ class Creator(BaseCreator): instance.transient_data.update({ "stagingDir": staging_dir_path, "stagingDir_persistent": staging_dir_info.is_persistent, + "stagingDir_is_custom": staging_dir_info.is_custom, }) self.log.info(f"Applied staging dir to instance: {staging_dir_path}") From 2be4d7c2e8b7ff7d9d133c60d3eb0141640ba3f3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Jan 2025 16:03:30 +0100 Subject: [PATCH 437/546] Disallow work area save as and browse if not in active task --- client/ayon_core/tools/workfiles/widgets/files_widget.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget.py b/client/ayon_core/tools/workfiles/widgets/files_widget.py index 16f0b6fce3..c5b5047c83 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget.py @@ -136,6 +136,8 @@ class FilesWidget(QtWidgets.QWidget): # Initial setup workarea_btn_open.setEnabled(False) + workarea_btn_browse.setEnabled(False) + workarea_btn_save.setEnabled(False) published_btn_copy_n_open.setEnabled(False) published_btn_change_context.setEnabled(False) published_btn_cancel.setVisible(False) @@ -264,6 +266,9 @@ class FilesWidget(QtWidgets.QWidget): def _on_workarea_path_changed(self, event): valid_path = event["path"] is not None self._workarea_btn_open.setEnabled(valid_path) + valid_task = self._selected_task_id is not None + self._workarea_btn_save.setEnabled(valid_task) + self._workarea_btn_browse.setEnabled(valid_task) # ------------------------------------------------------------- # Published workfiles From 44da46411c765d92611692e870dd6ef9322bfd1a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 14 Jan 2025 16:18:42 +0100 Subject: [PATCH 438/546] Move logic to correct location --- client/ayon_core/tools/workfiles/widgets/files_widget.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget.py b/client/ayon_core/tools/workfiles/widgets/files_widget.py index c5b5047c83..dbe5966c31 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget.py @@ -266,9 +266,6 @@ class FilesWidget(QtWidgets.QWidget): def _on_workarea_path_changed(self, event): valid_path = event["path"] is not None self._workarea_btn_open.setEnabled(valid_path) - valid_task = self._selected_task_id is not None - self._workarea_btn_save.setEnabled(valid_task) - self._workarea_btn_browse.setEnabled(valid_task) # ------------------------------------------------------------- # Published workfiles @@ -283,8 +280,9 @@ class FilesWidget(QtWidgets.QWidget): self._published_btn_change_context.setEnabled(enabled) def _update_workarea_btns_state(self): - enabled = self._is_save_enabled + enabled = self._is_save_enabled and self._valid_selected_context self._workarea_btn_save.setEnabled(enabled) + self._workarea_btn_browse.setEnabled(self._valid_selected_context) def _on_published_repre_changed(self, event): self._valid_representation_id = event["representation_id"] is not None @@ -299,6 +297,7 @@ class FilesWidget(QtWidgets.QWidget): and self._selected_task_id is not None ) self._update_published_btns_state() + self._update_workarea_btns_state() def _on_published_save_clicked(self): result = self._exec_save_as_dialog() From 6ed19f26114e6e3f738ec9b86b3a653e91586875 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 14 Jan 2025 17:38:29 +0100 Subject: [PATCH 439/546] Append fix for retiming image sequence. --- client/ayon_core/pipeline/editorial.py | 6 +- .../editorial/resources/img_seq_2x_speed.json | 160 ++++++++ .../resources/img_seq_2x_speed_resolve.json | 369 ++++++++++++++++++ .../test_media_range_with_retimes.py | 60 +++ 4 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed_resolve.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 0e9ee57bf9..c027d90d47 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -403,9 +403,13 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): rate=src_duration.rate ) + retimed_duration = otio.opentime.RationalTime( + src_duration.value * abs(time_scalar), + src_duration.rate + ) trim_range = otio.opentime.TimeRange( start_time=src_in + offset_in, - duration=src_duration + offset_duration + duration=retimed_duration + offset_duration ) # preserve discrete frame numbers diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed.json new file mode 100644 index 0000000000..80dfa34d4c --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed.json @@ -0,0 +1,160 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 909986.0387191772 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "Speed", + "effect_name": "LinearTimeWarp", + "time_scalar": 2.0 + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_retime_2x/sh010\", \"task\": null, \"clip_index\": \"37BA620A-6580-A543-ADF3-5A7133F41BB6\", \"hierarchy\": \"shots/hiero_retime_2x\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_retime_2x\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_retime_2x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_retime_2x\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"c60086c3-9ec3-448a-9bc5-6aa9f6af0fd5\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_retime_2x/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"8cdde735-d5a7-4f95-9cff-ded20ff21135\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 176.0, \"sourceOut\": 196.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_retime_2x/sh010\", \"task\": null, \"clip_index\": \"37BA620A-6580-A543-ADF3-5A7133F41BB6\", \"hierarchy\": \"shots/hiero_retime_2x\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_retime_2x\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_retime_2x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_retime_2x\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"c60086c3-9ec3-448a-9bc5-6aa9f6af0fd5\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"8cdde735-d5a7-4f95-9cff-ded20ff21135\", \"label\": \"/shots/hiero_retime_2x/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"064a92fc-5704-4316-8cc9-780e430ae2e5\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_retime_2x/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"37BA620A-6580-A543-ADF3-5A7133F41BB6\"}", + "label": "AYONdata_3c3f54af", + "note": "AYON data container" + }, + "name": "AYONdata_3c3f54af", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "301", + "foundry.source.filename": "output.%07d.exr 948674-948974", + "foundry.source.filesize": "", + "foundry.source.fragments": "301", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.%07d.exr 948674-948974", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%07d.exr 948674-948974", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "948674", + "foundry.source.timecode": "948674", + "foundry.source.umid": "28c4702f-5af7-4980-52c9-6eb875968890", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "301", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1278,718", + "media.exr.displayWindow": "0,0,1279,719", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2025-01-13 14:26:25", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.0948674.exr", + "media.input.filereader": "exr", + "media.input.filesize": "214941", + "media.input.frame": "1", + "media.input.height": "720", + "media.input.mtime": "2025-01-13 14:26:25", + "media.input.width": "1280", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "b13e3153b31d8f14", + "media.nuke.version": "15.0v5", + "padding": 7 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 301.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 948674.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 948674, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 7, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed_resolve.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed_resolve.json new file mode 100644 index 0000000000..07daaf1548 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_speed_resolve.json @@ -0,0 +1,369 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": { + "Resolve_OTIO": {} + }, + "name": "output.[1001-1099].tif", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 39.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "", + "effect_name": "", + "time_scalar": 2.0 + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Transform", + "Enabled": true, + "Name": "Transform", + "Parameters": [], + "Type": 2 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Cropping", + "Enabled": true, + "Name": "Cropping", + "Parameters": [], + "Type": 3 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Dynamic Zoom", + "Enabled": false, + "Name": "Dynamic Zoom", + "Parameters": [ + { + "Default Parameter Value": [ + 0.0, + 0.0 + ], + "Key Frames": { + "-19": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + "981": { + "Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + } + }, + "Parameter ID": "dynamicZoomCenter", + "Parameter Value": [ + 0.0, + 0.0 + ], + "Variant Type": "POINTF" + }, + { + "Default Parameter Value": 1.0, + "Key Frames": { + "-19": { + "Value": 0.8, + "Variant Type": "Double" + }, + "981": { + "Value": 1.0, + "Variant Type": "Double" + } + }, + "Parameter ID": "dynamicZoomScale", + "Parameter Value": 1.0, + "Variant Type": "Double", + "maxValue": 100.0, + "minValue": 0.01 + } + ], + "Type": 59 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Composite", + "Enabled": true, + "Name": "Composite", + "Parameters": [], + "Type": 1 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Lens Correction", + "Enabled": true, + "Name": "Lens Correction", + "Parameters": [], + "Type": 43 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Retime and Scaling", + "Enabled": true, + "Name": "Retime and Scaling", + "Parameters": [], + "Type": 22 + } + }, + "name": "", + "effect_name": "Resolve Effect" + }, + { + "OTIO_SCHEMA": "Effect.1", + "metadata": { + "Resolve_OTIO": { + "Effect Name": "Video Faders", + "Enabled": true, + "Name": "Video Faders", + "Parameters": [], + "Type": 36 + } + }, + "name": "", + "effect_name": "Resolve Effect" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "Resolve_OTIO": { + "Keywords": [], + "Note": "{\"resolve_sub_products\": {\"io.ayon.creators.resolve.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.shot\", \"variant\": \"Main\", \"folderPath\": \"/shots/resolve_2x/sh010\", \"task\": null, \"clip_variant\": \"\", \"clip_index\": \"51983d2a-8a54-45fc-b17d-b837bdcb2545\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"/shots/resolve_2x/sh010\", \"episode\": \"ep01\", \"sequence\": \"resolve_2x\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/resolve_2x\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"resolve_2x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"resolve_2x\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"04cd97b0-7e6e-4f58-b8b1-5f1956d53bfb\", \"reviewTrack\": \"Video 1\", \"label\": \"/shots/resolve_2x/sh010 shot\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"cc8b970c-69c1-4eab-b94f-ae41358a80ba\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 86400, \"clipOut\": 86411, \"clipDuration\": 11, \"sourceIn\": 19, \"sourceOut\": 30, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.resolve.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.resolve.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/resolve_2x/sh010\", \"task\": null, \"clip_variant\": \"\", \"clip_index\": \"51983d2a-8a54-45fc-b17d-b837bdcb2545\", \"clip_source_resolution\": {\"width\": \"1920\", \"height\": \"1080\", \"pixelAspect\": 1.0}, \"folder\": \"/shots/resolve_2x/sh010\", \"episode\": \"ep01\", \"sequence\": \"resolve_2x\", \"track\": \"{_track_}\", \"shot\": \"sh###\", \"hierarchy\": \"shots/resolve_2x\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"folder_type\": \"sequence\", \"entity_name\": \"resolve_2x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"resolve_2x\", \"track\": \"Video_1\", \"shot\": \"sh010\"}, \"heroTrack\": true, \"uuid\": \"04cd97b0-7e6e-4f58-b8b1-5f1956d53bfb\", \"reviewTrack\": \"Video 1\", \"parent_instance_id\": \"cc8b970c-69c1-4eab-b94f-ae41358a80ba\", \"label\": \"/shots/resolve_2x/sh010 plate\", \"has_promised_context\": true, \"newHierarchyIntegration\": true, \"newAssetPublishing\": true, \"instance_id\": \"564ef731-c518-4c8f-918d-b27d0c35856c\", \"creator_attributes\": {\"parentInstance\": \"/shots/resolve_2x/sh010 shot\", \"vSyncOn\": false, \"vSyncTrack\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"51983d2a-8a54-45fc-b17d-b837bdcb2545\", \"publish\": true}" + }, + "clip_index": "51983d2a-8a54-45fc-b17d-b837bdcb2545", + "publish": true, + "resolve_sub_products": { + "io.ayon.creators.resolve.plate": { + "active": true, + "clip_index": "51983d2a-8a54-45fc-b17d-b837bdcb2545", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "parentInstance": "/shots/resolve_2x/sh010 shot", + "vSyncOn": false, + "vSyncTrack": "Video 1" + }, + "creator_identifier": "io.ayon.creators.resolve.plate", + "episode": "ep01", + "folder": "/shots/resolve_2x/sh010", + "folderPath": "/shots/resolve_2x/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/resolve_2x", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "resolve_2x", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "564ef731-c518-4c8f-918d-b27d0c35856c", + "label": "/shots/resolve_2x/sh010 plate", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "cc8b970c-69c1-4eab-b94f-ae41358a80ba", + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "resolve_2x", + "folder_type": "sequence" + } + ], + "productName": "plateVideo_1", + "productType": "plate", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video 1", + "sequence": "resolve_2x", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "04cd97b0-7e6e-4f58-b8b1-5f1956d53bfb", + "variant": "Video_1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.resolve.shot": { + "active": true, + "clip_index": "51983d2a-8a54-45fc-b17d-b837bdcb2545", + "clip_source_resolution": { + "height": "1080", + "pixelAspect": 1.0, + "width": "1920" + }, + "clip_variant": "", + "creator_attributes": { + "clipDuration": 11, + "clipIn": 86400, + "clipOut": 86411, + "fps": "from_selection", + "frameEnd": 1012, + "frameStart": 1001, + "handleEnd": 10, + "handleStart": 10, + "sourceIn": 19, + "sourceOut": 30, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.resolve.shot", + "episode": "ep01", + "folder": "/shots/resolve_2x/sh010", + "folderPath": "/shots/resolve_2x/sh010", + "handleEnd": 10, + "handleStart": 10, + "has_promised_context": true, + "heroTrack": true, + "hierarchy": "shots/resolve_2x", + "hierarchyData": { + "episode": "ep01", + "folder": "shots", + "sequence": "resolve_2x", + "shot": "sh010", + "track": "Video_1" + }, + "id": "pyblish.avalon.instance", + "instance_id": "cc8b970c-69c1-4eab-b94f-ae41358a80ba", + "label": "/shots/resolve_2x/sh010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "shots", + "folder_type": "folder" + }, + { + "entity_name": "resolve_2x", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish_attributes": { + "CollectSlackFamilies": { + "additional_message": "" + } + }, + "reviewTrack": "Video 1", + "sequence": "resolve_2x", + "shot": "sh###", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "uuid": "04cd97b0-7e6e-4f58-b8b1-5f1956d53bfb", + "variant": "Main", + "workfileFrameStart": 1001 + } + } + }, + "name": "AYONData", + "color": "GREEN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 24.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": {}, + "name": "output.[1001-1099].tif", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 99.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:\\Users\\robin\\OneDrive\\Bureau\\dev_ayon\\data\\img_sequence\\tif", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1001, + "frame_step": 1, + "rate": 24.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 2e7193444c..da257eca58 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -291,3 +291,63 @@ def test_img_sequence_reverse_speed_from_24_to_23_976fps(): handle_start=10, handle_end=10, ) + + +def test_img_sequence_2x_speed(): + """ + Img sequence clip + available files = 948674-948974 25fps + source_range = 948850-948870 23.976fps + speed = 2.0 + """ + expected_data = { + 'mediaIn': 948850, + 'mediaOut': 948871, + 'handleStart': 20, + 'handleEnd': 20, + 'speed': 2.0, + 'versionData': { + 'retime': True, + 'speed': 2.0, + 'timewarps': [], + 'handleStart': 20, + 'handleEnd': 20 + } + } + + _check_expected_retimed_values( + "img_seq_2x_speed.json", + expected_data, + handle_start=10, + handle_end=10, + ) + + +def test_img_sequence_2x_speed_resolve(): + """ + Img sequence clip + available files = 0-99 24fps + source_range = 38-49 24fps + speed = 2.0 + """ + expected_data = { + 'mediaIn': 1040, + 'mediaOut': 1061, + 'handleStart': 20, + 'handleEnd': 20, + 'speed': 2.0, + 'versionData': { + 'retime': True, + 'speed': 2.0, + 'timewarps': [], + 'handleStart': 20, + 'handleEnd': 20 + } + } + + _check_expected_retimed_values( + "img_seq_2x_speed_resolve.json", + expected_data, + handle_start=10, + handle_end=10, + ) From 76c7d90c8c1c8c1eeb81c893adead84f6be5f7c7 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 14 Jan 2025 18:15:01 +0100 Subject: [PATCH 440/546] Add fix + test for image sequence frozen frame. --- client/ayon_core/pipeline/editorial.py | 8 +- .../resources/img_seq_freeze_frame.json | 160 ++++++++++++++++++ .../test_media_range_with_retimes.py | 31 ++++ 3 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_freeze_frame.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index c027d90d47..9a241087f6 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -256,8 +256,14 @@ def remap_range_on_file_sequence(otio_clip, otio_range): rate=available_range_rate, ).to_frames() + # e.g.: + # duration = 10 frames at 24fps + # if frame_in = 1001 then + # frame_out = 1010 + offset_duration = max(0, otio_range.duration.to_frames() - 1) + frame_out = otio.opentime.RationalTime.from_frames( - frame_in + otio_range.duration.to_frames() - 1, + frame_in + offset_duration, rate=available_range_rate, ).to_frames() diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_freeze_frame.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_freeze_frame.json new file mode 100644 index 0000000000..05b48370b2 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_freeze_frame.json @@ -0,0 +1,160 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 5.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 909990.8339241028 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "FreezeFrame.1", + "metadata": {}, + "name": "FreezeFrame", + "effect_name": "FreezeFrame", + "time_scalar": 0.0 + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_freeze_frame/sh010\", \"task\": null, \"clip_index\": \"85ABEEEA-6A90-CE47-9DE2-73BAB11EE31D\", \"hierarchy\": \"shots/hiero_freeze_frame\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_freeze_frame\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_freeze_frame\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_freeze_frame\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"08ba1c0a-fc51-4275-b6c8-1cb81381b043\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_freeze_frame/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"892de813-fc78-4d92-b25f-4ea5c4791bb8\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1006, \"clipIn\": 0, \"clipOut\": 4, \"clipDuration\": 5, \"sourceIn\": 181.0, \"sourceOut\": 181.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_freeze_frame/sh010\", \"task\": null, \"clip_index\": \"85ABEEEA-6A90-CE47-9DE2-73BAB11EE31D\", \"hierarchy\": \"shots/hiero_freeze_frame\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_freeze_frame\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_freeze_frame\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_freeze_frame\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"08ba1c0a-fc51-4275-b6c8-1cb81381b043\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"892de813-fc78-4d92-b25f-4ea5c4791bb8\", \"label\": \"/shots/hiero_freeze_frame/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"24eb8386-4c42-4439-ac41-17ec4efb0073\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_freeze_frame/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"85ABEEEA-6A90-CE47-9DE2-73BAB11EE31D\"}", + "label": "AYONdata_a8304fcf", + "note": "AYON data container" + }, + "name": "AYONdata_a8304fcf", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "301", + "foundry.source.filename": "output.%07d.exr 948674-948974", + "foundry.source.filesize": "", + "foundry.source.fragments": "301", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.%07d.exr 948674-948974", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%07d.exr 948674-948974", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "948674", + "foundry.source.timecode": "948674", + "foundry.source.umid": "28c4702f-5af7-4980-52c9-6eb875968890", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "301", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1278,718", + "media.exr.displayWindow": "0,0,1279,719", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2025-01-13 14:26:25", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.0948674.exr", + "media.input.filereader": "exr", + "media.input.filesize": "214941", + "media.input.frame": "1", + "media.input.height": "720", + "media.input.mtime": "2025-01-13 14:26:25", + "media.input.width": "1280", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "b13e3153b31d8f14", + "media.nuke.version": "15.0v5", + "padding": 7 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 301.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 948674.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 948674, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 7, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index da257eca58..1041dfe4dd 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -351,3 +351,34 @@ def test_img_sequence_2x_speed_resolve(): handle_start=10, handle_end=10, ) + + +def test_img_sequence_frozen_frame(): + """ + Img sequence clip + available files = 948674-948974 25fps + source_range = 909990.8339241028 + - 909995.8339241028 23.976fps + speed = 0.0 + """ + expected_data = { + 'mediaIn': 948855, + 'mediaOut': 948855, + 'handleStart': 0, + 'handleEnd': 0, + 'speed': 0.0, + 'versionData': { + 'retime': True, + 'speed': 0.0, + 'timewarps': [], + 'handleStart': 0, + 'handleEnd': 0, + } + } + + _check_expected_retimed_values( + "img_seq_freeze_frame.json", + expected_data, + handle_start=10, + handle_end=10, + ) From 1123d31750260fef569df2217990a7daa8157956 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 15 Jan 2025 11:18:23 +0100 Subject: [PATCH 441/546] Add fixes and tests for qt retime and frozen frame. --- client/ayon_core/pipeline/editorial.py | 13 +- .../publish/collect_otio_subset_resources.py | 14 +- .../editorial/resources/qt_freeze_frame.json | 159 +++++++++++++++++ .../resources/qt_reverse_speed_2x.json | 160 ++++++++++++++++++ .../test_media_range_with_retimes.py | 64 +++++++ 5 files changed, 400 insertions(+), 10 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_freeze_frame.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_2x.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 9a241087f6..f5a876b5ba 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -429,13 +429,12 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): else: # compute retimed range media_in_trimmed = conformed_source_range.start_time.value + offset_in - media_out_trimmed = media_in_trimmed + ( - ( - conformed_source_range.duration.value - * abs(time_scalar) - + offset_out - ) - 1 - ) + + offset_duration = conformed_source_range.duration.value * abs(time_scalar) + offset_duration += offset_out + offset_duration -= 1 # duration 1 frame -> freeze frame -> end = start + 0 + offset_duration = max(0, offset_duration) # negative duration = frozen frame + media_out_trimmed = media_in_trimmed + offset_duration media_in = available_range.start_time.value media_out = available_range.end_time_inclusive().value diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 0fb30326c6..ed7e8ac4f1 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -174,9 +174,17 @@ class CollectOtioSubsetResources( path, trimmed_media_range_h, metadata) self.staging_dir, collection = collection_data - self.log.debug(collection) - repre = self._create_representation( - frame_start, frame_end, collection=collection) + if len(collection.indexes) > 1: + self.log.debug(collection) + repre = self._create_representation( + frame_start, frame_end, collection=collection) + else: + filename, = tuple(collection) + self.log.debug(filename) + + # TODO: discuss this, it erases frame number. + repre = self._create_representation( + frame_start, frame_end, file=filename) if ( not instance.data.get("otioReviewClips") diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_freeze_frame.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_freeze_frame.json new file mode 100644 index 0000000000..de665eaea7 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_freeze_frame.json @@ -0,0 +1,159 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 29.970030784606934 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "FreezeFrame.1", + "metadata": {}, + "name": "FreezeFrame", + "effect_name": "FreezeFrame", + "time_scalar": 0.0 + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_qt_freeze_frame/sh010\", \"task\": null, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\", \"hierarchy\": \"shots/hiero_qt_freeze_frame\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_freeze_frame\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_qt_freeze_frame\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_freeze_frame\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"e896c630-8c44-408f-a1a0-bffbe330dbe9\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_qt_freeze_frame/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"75b9112f-6357-4235-8a74-252467d6553d\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 30.0, \"sourceOut\": 30.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_qt_freeze_frame/sh010\", \"task\": null, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\", \"hierarchy\": \"shots/hiero_qt_freeze_frame\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_freeze_frame\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_qt_freeze_frame\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_freeze_frame\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"e896c630-8c44-408f-a1a0-bffbe330dbe9\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"75b9112f-6357-4235-8a74-252467d6553d\", \"label\": \"/shots/hiero_qt_freeze_frame/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"95912bf0-aa1c-47ae-a821-fef410c32687\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_qt_freeze_frame/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\"}", + "label": "AYONdata_e681ec48", + "note": "AYON data container" + }, + "name": "AYONdata_e681ec48", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Gamma2.2", + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1920, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "error", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "default (Rec 709)", + "com.apple.quicktime.codec": "H.264", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "Gamma2.2", + "foundry.source.duration": "101", + "foundry.source.filename": "qt_no_tc_24fps.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float32) Open Color IO space: 114", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shoottime": "4294967295", + "foundry.source.shortfilename": "qt_no_tc_24fps.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "0", + "foundry.source.type": "QuickTime H.264", + "foundry.source.umid": "16634e88-6450-4727-6c6e-501f4b31b637", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Gamma2.2", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.samplerate": "Invalid", + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-25 17:16:12", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov", + "media.input.filereader": "mov64", + "media.input.filesize": "14631252", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-25 17:16:16", + "media.input.pixel_aspect": "1", + "media.input.timecode": "00:00:00:00", + "media.input.width": "1920", + "media.quicktime.codec_id": "avc1", + "media.quicktime.codec_name": "h264", + "media.quicktime.encoder": "H.264", + "media.quicktime.nclc_matrix": "BT709", + "media.quicktime.nclc_primaries": "ITU-R BT.709", + "media.quicktime.nclc_transfer_function": "ITU-R BT.709", + "media.quicktime.thefoundry.Application": "Nuke", + "media.quicktime.thefoundry.ApplicationVersion": "15.0v5", + "media.quicktime.thefoundry.Colorspace": "Gamma2.2", + "media.quicktime.thefoundry.Writer": "mov64", + "media.quicktime.thefoundry.YCbCrMatrix": "Rec 709", + "media.stream.pixel_format": "yuv420p", + "uk.co.thefoundry.Application": "Nuke", + "uk.co.thefoundry.ApplicationVersion": "15.0v5", + "uk.co.thefoundry.Colorspace": "Gamma2.2", + "uk.co.thefoundry.Writer": "mov64", + "uk.co.thefoundry.YCbCrMatrix": "Rec 709" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_2x.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_2x.json new file mode 100644 index 0000000000..467bb8d7a1 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_2x.json @@ -0,0 +1,160 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 29.970030784606934 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "Speed", + "effect_name": "LinearTimeWarp", + "time_scalar": -2.0 + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_qt_neg_2x/sh010\", \"task\": null, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\", \"hierarchy\": \"shots/hiero_qt_neg_2x\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg_2x\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_qt_neg_2x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg_2x\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"a59e3db6-0b60-41f5-827c-9d280547bf31\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_qt_neg_2x/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"f98ac652-ed03-4985-bad9-5b028ceeddba\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 50.0, \"sourceOut\": 30.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_qt_neg_2x/sh010\", \"task\": null, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\", \"hierarchy\": \"shots/hiero_qt_neg_2x\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg_2x\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_qt_neg_2x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg_2x\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"a59e3db6-0b60-41f5-827c-9d280547bf31\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"f98ac652-ed03-4985-bad9-5b028ceeddba\", \"label\": \"/shots/hiero_qt_neg_2x/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"5c9c047c-43fa-42a1-a00f-e9c9d6e5a3c4\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_qt_neg_2x/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\"}", + "label": "AYONdata_fd6d196e", + "note": "AYON data container" + }, + "name": "AYONdata_fd6d196e", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Gamma2.2", + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1920, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "error", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "H.264", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "Gamma2.2", + "foundry.source.duration": "101", + "foundry.source.filename": "qt_no_tc_24fps.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float32) Open Color IO space: 114", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shoottime": "4294967295", + "foundry.source.shortfilename": "qt_no_tc_24fps.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "0", + "foundry.source.type": "QuickTime H.264", + "foundry.source.umid": "16634e88-6450-4727-6c6e-501f4b31b637", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Gamma2.2", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.samplerate": "Invalid", + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-25 17:16:12", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov", + "media.input.filereader": "mov64", + "media.input.filesize": "14631252", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-25 17:16:16", + "media.input.pixel_aspect": "1", + "media.input.timecode": "00:00:00:00", + "media.input.width": "1920", + "media.quicktime.codec_id": "avc1", + "media.quicktime.codec_name": "h264", + "media.quicktime.encoder": "H.264", + "media.quicktime.nclc_matrix": "BT709", + "media.quicktime.nclc_primaries": "ITU-R BT.709", + "media.quicktime.nclc_transfer_function": "ITU-R BT.709", + "media.quicktime.thefoundry.Application": "Nuke", + "media.quicktime.thefoundry.ApplicationVersion": "15.0v5", + "media.quicktime.thefoundry.Colorspace": "Gamma2.2", + "media.quicktime.thefoundry.Writer": "mov64", + "media.quicktime.thefoundry.YCbCrMatrix": "Rec 709", + "media.stream.pixel_format": "yuv420p", + "uk.co.thefoundry.Application": "Nuke", + "uk.co.thefoundry.ApplicationVersion": "15.0v5", + "uk.co.thefoundry.Colorspace": "Gamma2.2", + "uk.co.thefoundry.Writer": "mov64", + "uk.co.thefoundry.YCbCrMatrix": "Rec 709" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 1041dfe4dd..d4badd8345 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -113,6 +113,70 @@ def test_movie_retime_effect(): ) +def test_movie_reverse_speed_2x(): + """ + Movie clip (no timecode) + available files = 0-100 24fps + source_range = 29.97-40.97 23.976fps + speed = -2.0 + """ + expected_data = { + # not exactly 30 because of 23.976 rouding + 'mediaIn': 30.000000000000004, + # not exactly 50 because of 23.976 rouding + 'mediaOut': 51.02199940144827, + 'handleStart': 20, + 'handleEnd': 20, + 'speed': -2.0, + 'versionData': { + 'retime': True, + 'speed': -2.0, + 'timewarps': [], + 'handleStart': 20, + 'handleEnd': 20, + } + } + + _check_expected_retimed_values( + "qt_reverse_speed_2x.json", + expected_data, + handle_start=10, + handle_end=10, + ) + + + +def test_movie_frozen_frame(): + """ + Movie clip (no timecode) + available files = 0-100 24fps + source_range = 29.97-40.97 23.976fps + speed = 0.0 + """ + expected_data = { + # not exactly 30 because of OTIO rounding + 'mediaIn': 30.000000000000004, + 'mediaOut': 30.000000000000004, + 'handleStart': 0, + 'handleEnd': 0, + 'speed': 0.0, + 'versionData': { + 'retime': True, + 'speed': 0.0, + 'timewarps': [], + 'handleStart': 0, + 'handleEnd': 0, + } + } + + _check_expected_retimed_values( + "qt_freeze_frame.json", + expected_data, + handle_start=10, + handle_end=10, + ) + + def test_img_sequence_no_handles(): """ Img sequence clip (no embedded timecode) From 704ef232b6c20ac7f0767f0a997e11092986eb94 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 15 Jan 2025 13:20:22 +0100 Subject: [PATCH 442/546] Edit link to OTIO issue. --- .../pipeline/editorial/test_media_range_with_retimes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index d4badd8345..c045379806 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -122,8 +122,9 @@ def test_movie_reverse_speed_2x(): """ expected_data = { # not exactly 30 because of 23.976 rouding + # https://github.com/AcademySoftwareFoundation/ + # OpenTimelineIO/issues/1822 'mediaIn': 30.000000000000004, - # not exactly 50 because of 23.976 rouding 'mediaOut': 51.02199940144827, 'handleStart': 20, 'handleEnd': 20, @@ -155,6 +156,8 @@ def test_movie_frozen_frame(): """ expected_data = { # not exactly 30 because of OTIO rounding + # https://github.com/AcademySoftwareFoundation/ + # OpenTimelineIO/issues/1822 'mediaIn': 30.000000000000004, 'mediaOut': 30.000000000000004, 'handleStart': 0, From b185972a954f7111591576b90ee3436a7b775bf2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 15 Jan 2025 14:02:20 +0100 Subject: [PATCH 443/546] Avoid database queries when collecting managed staging dir --- client/ayon_core/pipeline/publish/lib.py | 1 + client/ayon_core/pipeline/staging_dir.py | 5 ++++- client/ayon_core/pipeline/template_data.py | 13 ++++++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 40a9b47aba..25495ed38b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -708,6 +708,7 @@ def get_instance_staging_dir(instance): project_settings=context.data["project_settings"], template_data=template_data, always_return_path=True, + username=context.data["user"], ) staging_dir_path = staging_dir_info.directory diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 37d6b955e2..2004096bd0 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -130,6 +130,7 @@ def get_staging_dir_info( logger: Optional[logging.Logger] = None, prefix: Optional[str] = None, suffix: Optional[str] = None, + username: Optional[str] = None, ) -> Optional[StagingDir]: """Get staging dir info data. @@ -183,7 +184,9 @@ def get_staging_dir_info( # making few queries to database ctx_data = get_template_data( - project_entity, folder_entity, task_entity, host_name + project_entity, folder_entity, task_entity, host_name, + settings=project_settings, + username=username ) # add additional data diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index c7aa46fd62..0a95a98be8 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -4,7 +4,7 @@ from ayon_core.settings import get_studio_settings from ayon_core.lib.local_settings import get_ayon_username -def get_general_template_data(settings=None): +def get_general_template_data(settings=None, username=None): """General template data based on system settings or machine. Output contains formatting keys: @@ -14,17 +14,22 @@ def get_general_template_data(settings=None): Args: settings (Dict[str, Any]): Studio or project settings. + username (Optional[str]): AYON Username. """ if not settings: settings = get_studio_settings() + + if username is None: + username = get_ayon_username() + core_settings = settings["core"] return { "studio": { "name": core_settings["studio_name"], "code": core_settings["studio_code"] }, - "user": get_ayon_username() + "user": username } @@ -145,6 +150,7 @@ def get_template_data( task_entity=None, host_name=None, settings=None, + username=None ): """Prepare data for templates filling from entered documents and info. @@ -167,12 +173,13 @@ def get_template_data( host_name (Optional[str]): Used to fill '{app}' key. settings (Union[Dict, None]): Prepared studio or project settings. They're queried if not passed (may be slower). + username (Optional[str]): AYON Username. Returns: Dict[str, Any]: Data prepared for filling workdir template. """ - template_data = get_general_template_data(settings) + template_data = get_general_template_data(settings, username=username) template_data.update(get_project_template_data(project_entity)) if folder_entity: template_data.update(get_folder_template_data( From 8351395a8279f09d08e1ec0097bb39402882ab8f Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 15 Jan 2025 18:43:02 +0100 Subject: [PATCH 444/546] Consolidate timewarp computation and add tests. --- client/ayon_core/pipeline/editorial.py | 48 +++- .../resources/img_seq_2x_time_warp.json | 181 ++++++++++++++ .../resources/img_seq_multiple_tws.json | 216 ++++++++++++++++ .../resources/img_seq_tw_beyond_range.json | 174 +++++++++++++ .../editorial/resources/qt_timewarp.json | 174 +++++++++++++ .../test_media_range_with_retimes.py | 235 ++++++++++++++++++ 6 files changed, 1015 insertions(+), 13 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_time_warp.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_multiple_tws.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_tw_beyond_range.json create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_timewarp.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index f5a876b5ba..ed1cdf9974 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -375,10 +375,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): tw_node.update(metadata) tw_node["lookup"] = list(lookup) - # get first and last frame offsets - offset_in += lookup[0] - offset_out += lookup[-1] - # add to timewarp nodes time_warp_nodes.append(tw_node) @@ -403,19 +399,14 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): src_in = conformed_source_range.start_time src_duration = conformed_source_range.duration - offset_in = otio.opentime.RationalTime(offset_in, rate=src_in.rate) - offset_duration = otio.opentime.RationalTime( - offset_out, - rate=src_duration.rate - ) - retimed_duration = otio.opentime.RationalTime( src_duration.value * abs(time_scalar), src_duration.rate ) trim_range = otio.opentime.TimeRange( - start_time=src_in + offset_in, - duration=retimed_duration + offset_duration + start_time=src_in, + duration=retimed_duration, + ) # preserve discrete frame numbers @@ -431,7 +422,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in_trimmed = conformed_source_range.start_time.value + offset_in offset_duration = conformed_source_range.duration.value * abs(time_scalar) - offset_duration += offset_out offset_duration -= 1 # duration 1 frame -> freeze frame -> end = start + 0 offset_duration = max(0, offset_duration) # negative duration = frozen frame media_out_trimmed = media_in_trimmed + offset_duration @@ -439,6 +429,38 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): media_in = available_range.start_time.value media_out = available_range.end_time_inclusive().value + if time_warp_nodes: + # Naive approach: Resolve consecutive timewarp(s) on range, + # then check if plate range has to be extended beyond source range. + frame_range = [media_in_trimmed] + in_frame = media_in_trimmed + time_scalar + while in_frame <= media_out_trimmed: + frame_range.append(in_frame) + in_frame += time_scalar + + for tw_idx, tw in enumerate(time_warp_nodes): + for idx, frame_number in enumerate(frame_range): + # First timewarp, apply on media range + if tw_idx == 0: + frame_range[idx] = round(frame_number + tw["lookup"][idx] * time_scalar) + # Consecutive timewarp, apply on the previous result + else: + new_idx = round(idx + tw["lookup"][idx]) + + if not 0 <= new_idx < len(frame_range): + # TODO: implementing this would need to actually have + # retiming engine resolve process within AYON, resolving wraps + # as curves, then projecting those into the previous media_range. + raise NotImplementedError( + "Unsupported consecutive timewarps (out of computed range)" + ) + + frame_range[idx] = frame_range[new_idx] + + # adjust range if needed + media_in_trimmed = min(media_in_trimmed, min(frame_range)) + media_out_trimmed = max(media_out_trimmed, max(frame_range)) + # adjust available handles if needed if (media_in_trimmed - media_in) < handle_start: handle_start = max(0, media_in_trimmed - media_in) diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_time_warp.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_time_warp.json new file mode 100644 index 0000000000..0876dcd179 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_2x_time_warp.json @@ -0,0 +1,181 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 909986.0387191772 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "Speed", + "effect_name": "LinearTimeWarp", + "time_scalar": 2.0 + }, + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 4.0, + "lookup": [ + 2.0, + 1.7039999923706057, + 1.431999991416931, + 1.2079999942779533, + 1.055999998092652, + 1.0, + 1.056000007629395, + 1.208000022888184, + 1.432000034332276, + 1.7040000305175766, + 2.0 + ] + }, + "name": "TimeWarp6", + "effect_name": "TimeWarp" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_img_seq_tw_speed/sh010\", \"task\": null, \"clip_index\": \"699C12C3-07B7-E74E-A8BC-07554560B91E\", \"hierarchy\": \"shots/hiero_img_seq_tw_speed\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_img_seq_tw_speed\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 0, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_img_seq_tw_speed\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_img_seq_tw_speed\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"731977d8-6f06-415d-9086-b04b58a16ce3\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_img_seq_tw_speed/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"d157ce1c-3157-4a34-a8b5-14c881387239\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 0, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 176.0, \"sourceOut\": 196.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_img_seq_tw_speed/sh010\", \"task\": null, \"clip_index\": \"699C12C3-07B7-E74E-A8BC-07554560B91E\", \"hierarchy\": \"shots/hiero_img_seq_tw_speed\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_img_seq_tw_speed\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 0, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_img_seq_tw_speed\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_img_seq_tw_speed\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"731977d8-6f06-415d-9086-b04b58a16ce3\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"parent_instance_id\": \"d157ce1c-3157-4a34-a8b5-14c881387239\", \"label\": \"/shots/hiero_img_seq_tw_speed/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"daf5d8e4-5698-4a41-90eb-05eea2992dff\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_img_seq_tw_speed/sh010 shotMain\", \"review\": false, \"reviewableSource\": \"clip_media\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"699C12C3-07B7-E74E-A8BC-07554560B91E\"}", + "label": "AYONdata_9f37cdbf", + "note": "AYON data container" + }, + "name": "AYONdata_9f37cdbf", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "301", + "foundry.source.filename": "output.%07d.exr 948674-948974", + "foundry.source.filesize": "", + "foundry.source.fragments": "301", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.%07d.exr 948674-948974", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%07d.exr 948674-948974", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "948674", + "foundry.source.timecode": "948674", + "foundry.source.umid": "28c4702f-5af7-4980-52c9-6eb875968890", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "301", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1278,718", + "media.exr.displayWindow": "0,0,1279,719", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2025-01-13 14:26:25", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.0948674.exr", + "media.input.filereader": "exr", + "media.input.filesize": "214941", + "media.input.frame": "1", + "media.input.height": "720", + "media.input.mtime": "2025-01-13 14:26:25", + "media.input.width": "1280", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "b13e3153b31d8f14", + "media.nuke.version": "15.0v5", + "padding": 7 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 301.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 948674.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 948674, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 7, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_multiple_tws.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_multiple_tws.json new file mode 100644 index 0000000000..88f2dbc86c --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_multiple_tws.json @@ -0,0 +1,216 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 909986.0387191772 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 1.0, + "lookup": [ + -5.0, + -3.9440000305175777, + -2.852000034332275, + -1.6880000228881844, + -0.4160000076293944, + 1.0, + 2.5839999923706056, + 4.311999977111817, + 6.147999965667726, + 8.055999969482421, + 10.0 + ] + }, + "name": "TimeWarp3", + "effect_name": "TimeWarp" + }, + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 1.0, + "lookup": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "name": "TimeWarp4", + "effect_name": "TimeWarp" + }, + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 1.0, + "lookup": [ + 0.0, + -1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0 + ] + }, + "name": "TimeWarp5", + "effect_name": "TimeWarp" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_seq_max_tw/sh010\", \"task\": null, \"clip_index\": \"4C055A68-8354-474A-A6F8-B0CBF9A537CD\", \"hierarchy\": \"shots/hiero_seq_max_tw\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_seq_max_tw\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"5e82a346-17c4-4ccb-a795-35e1a809b243\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_seq_max_tw/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"9cb2a119-8aa6-487e-a46b-9b9ff25323be\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 176.0, \"sourceOut\": 186.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_seq_max_tw/sh010\", \"task\": null, \"clip_index\": \"4C055A68-8354-474A-A6F8-B0CBF9A537CD\", \"hierarchy\": \"shots/hiero_seq_max_tw\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_seq_max_tw\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"5e82a346-17c4-4ccb-a795-35e1a809b243\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"parent_instance_id\": \"9cb2a119-8aa6-487e-a46b-9b9ff25323be\", \"label\": \"/shots/hiero_seq_max_tw/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"771e41ed-74b0-4fcc-882c-6a248d45a464\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_seq_max_tw/sh010 shotMain\", \"review\": false, \"reviewableSource\": \"clip_media\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"4C055A68-8354-474A-A6F8-B0CBF9A537CD\"}", + "label": "AYONdata_b6896763", + "note": "AYON data container" + }, + "name": "AYONdata_b6896763", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "301", + "foundry.source.filename": "output.%07d.exr 948674-948974", + "foundry.source.filesize": "", + "foundry.source.fragments": "301", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.%07d.exr 948674-948974", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%07d.exr 948674-948974", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "948674", + "foundry.source.timecode": "948674", + "foundry.source.umid": "28c4702f-5af7-4980-52c9-6eb875968890", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "301", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1278,718", + "media.exr.displayWindow": "0,0,1279,719", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2025-01-13 14:26:25", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.0948674.exr", + "media.input.filereader": "exr", + "media.input.filesize": "214941", + "media.input.frame": "1", + "media.input.height": "720", + "media.input.mtime": "2025-01-13 14:26:25", + "media.input.width": "1280", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "b13e3153b31d8f14", + "media.nuke.version": "15.0v5", + "padding": 7 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 301.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 948674.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 948674, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 7, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_tw_beyond_range.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_tw_beyond_range.json new file mode 100644 index 0000000000..1a753098d7 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_tw_beyond_range.json @@ -0,0 +1,174 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 909986.0387191772 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 1.0, + "lookup": [ + -5.0, + -3.9440000305175777, + -2.852000034332275, + -1.6880000228881844, + -0.4160000076293944, + 1.0, + 2.5839999923706056, + 4.311999977111817, + 6.147999965667726, + 8.055999969482421, + 10.0 + ] + }, + "name": "TimeWarp3", + "effect_name": "TimeWarp" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_seq_max_tw/sh010\", \"task\": null, \"clip_index\": \"4C055A68-8354-474A-A6F8-B0CBF9A537CD\", \"hierarchy\": \"shots/hiero_seq_max_tw\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_seq_max_tw\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"5e82a346-17c4-4ccb-a795-35e1a809b243\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_seq_max_tw/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"9cb2a119-8aa6-487e-a46b-9b9ff25323be\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 176.0, \"sourceOut\": 186.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_seq_max_tw/sh010\", \"task\": null, \"clip_index\": \"4C055A68-8354-474A-A6F8-B0CBF9A537CD\", \"hierarchy\": \"shots/hiero_seq_max_tw\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": null, \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_seq_max_tw\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_max_tw\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"5e82a346-17c4-4ccb-a795-35e1a809b243\", \"reviewTrack\": null, \"folderName\": \"sh010\", \"parent_instance_id\": \"9cb2a119-8aa6-487e-a46b-9b9ff25323be\", \"label\": \"/shots/hiero_seq_max_tw/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"771e41ed-74b0-4fcc-882c-6a248d45a464\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_seq_max_tw/sh010 shotMain\", \"review\": false, \"reviewableSource\": \"clip_media\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"4C055A68-8354-474A-A6F8-B0CBF9A537CD\"}", + "label": "AYONdata_b6896763", + "note": "AYON data container" + }, + "name": "AYONdata_b6896763", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "301", + "foundry.source.filename": "output.%07d.exr 948674-948974", + "foundry.source.filesize": "", + "foundry.source.fragments": "301", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.%07d.exr 948674-948974", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%07d.exr 948674-948974", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "948674", + "foundry.source.timecode": "948674", + "foundry.source.umid": "28c4702f-5af7-4980-52c9-6eb875968890", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "301", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1278,718", + "media.exr.displayWindow": "0,0,1279,719", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2025-01-13 14:26:25", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.0948674.exr", + "media.input.filereader": "exr", + "media.input.filesize": "214941", + "media.input.frame": "1", + "media.input.height": "720", + "media.input.mtime": "2025-01-13 14:26:25", + "media.input.width": "1280", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "b13e3153b31d8f14", + "media.nuke.version": "15.0v5", + "padding": 7 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 301.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 948674.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 948674, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 7, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_timewarp.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_timewarp.json new file mode 100644 index 0000000000..88ee7130f4 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_timewarp.json @@ -0,0 +1,174 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 909986.0387191772 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "TimeEffect.1", + "metadata": { + "length": 4.0, + "lookup": [ + 2.0, + 1.8959999809265136, + 1.767999971389771, + 1.59199997138977, + 1.3439999809265135, + 1.0, + 0.5440000181198119, + -0.007999974250793684, + -0.6319999756813051, + -1.3039999847412114, + -2.0 + ] + }, + "name": "TimeWarp2", + "effect_name": "TimeWarp" + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_seq_tw/sh010\", \"task\": null, \"clip_index\": \"27126150-EDFA-9F45-908C-59F5CD1A94E2\", \"hierarchy\": \"shots/hiero_seq_tw\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_tw\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_seq_tw\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_tw\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"5c0e0d32-fa09-4331-afbb-5b194cfa258c\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_seq_tw/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"b88fe40d-f92d-42b0-b7f6-7cb7a206e878\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceIn\": 176.0, \"sourceOut\": 186.0, \"fps\": \"from_selection\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_seq_tw/sh010\", \"task\": null, \"clip_index\": \"27126150-EDFA-9F45-908C-59F5CD1A94E2\", \"hierarchy\": \"shots/hiero_seq_tw\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_tw\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"Video 1\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_seq_tw\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_seq_tw\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"5c0e0d32-fa09-4331-afbb-5b194cfa258c\", \"reviewTrack\": \"Video 1\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"b88fe40d-f92d-42b0-b7f6-7cb7a206e878\", \"label\": \"/shots/hiero_seq_tw/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"e3ea1467-dfaf-48db-bf3c-6cbbbd2cd972\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_seq_tw/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"Video 1\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"27126150-EDFA-9F45-908C-59F5CD1A94E2\"}", + "label": "AYONdata_ef8f52f1", + "note": "AYON data container" + }, + "name": "AYONdata_ef8f52f1", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.colorspace": "ACES - ACES2065-1", + "ayon.source.height": 720, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1280, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "default", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "ACES - ACES2065-1", + "foundry.source.duration": "301", + "foundry.source.filename": "output.%07d.exr 948674-948974", + "foundry.source.filesize": "", + "foundry.source.fragments": "301", + "foundry.source.framerate": "25", + "foundry.source.fullpath": "", + "foundry.source.height": "720", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.%07d.exr 948674-948974", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float16) Open Color IO space: 11", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shortfilename": "output.%07d.exr 948674-948974", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "948674", + "foundry.source.timecode": "948674", + "foundry.source.umid": "28c4702f-5af7-4980-52c9-6eb875968890", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1280", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "ACES - ACES2065-1", + "foundry.timeline.duration": "301", + "foundry.timeline.framerate": "25", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.readParams": "AAAAAQAAAAAAAAAFAAAACmNvbG9yc3BhY2UAAAAFaW50MzIAAAAAAAAAC2VkZ2VfcGl4ZWxzAAAABWludDMyAAAAAAAAABFpZ25vcmVfcGFydF9uYW1lcwAAAARib29sAAAAAAhub3ByZWZpeAAAAARib29sAAAAAB5vZmZzZXRfbmVnYXRpdmVfZGlzcGxheV93aW5kb3cAAAAEYm9vbAE=", + "foundry.timeline.samplerate": "Invalid", + "isSequence": true, + "media.exr.channels": "B:{1 0 1 1},G:{1 0 1 1},R:{1 0 1 1}", + "media.exr.compression": "2", + "media.exr.compressionName": "Zip (1 scanline)", + "media.exr.dataWindow": "1,1,1278,718", + "media.exr.displayWindow": "0,0,1279,719", + "media.exr.lineOrder": "0", + "media.exr.pixelAspectRatio": "1", + "media.exr.screenWindowCenter": "0,0", + "media.exr.screenWindowWidth": "1", + "media.exr.type": "scanlineimage", + "media.exr.version": "1", + "media.input.bitsperchannel": "16-bit half float", + "media.input.ctime": "2025-01-13 14:26:25", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange/output.0948674.exr", + "media.input.filereader": "exr", + "media.input.filesize": "214941", + "media.input.frame": "1", + "media.input.height": "720", + "media.input.mtime": "2025-01-13 14:26:25", + "media.input.width": "1280", + "media.nuke.full_layer_names": "0", + "media.nuke.node_hash": "b13e3153b31d8f14", + "media.nuke.version": "15.0v5", + "padding": 7 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 301.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 948674.0 + } + }, + "available_image_bounds": null, + "target_url_base": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/img_sequence/exr_long_frameRange\\", + "name_prefix": "output.", + "name_suffix": ".exr", + "start_frame": 948674, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 7, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index c045379806..45cba64558 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -180,6 +180,55 @@ def test_movie_frozen_frame(): ) +def test_movie_timewarp(): + """ + Movie clip (no timecode) + available files = 0-100 24fps + source_range = 29.97-40.97 23.976fps + speed = timewarp + """ + expected_data = { + 'handleEnd': 10, + 'handleStart': 0, + 'mediaIn': 948850, + 'mediaOut': 948860, + 'speed': 1.0, + 'versionData': {'handleEnd': 10, + 'handleStart': 0, + 'retime': True, + 'speed': 1.0, + 'timewarps': [ + { + 'Class': 'TimeWarp', + 'length': 4.0, + 'lookup': [ + 2.0, + 1.8959999809265136, + 1.767999971389771, + 1.59199997138977, + 1.3439999809265135, + 1.0, + 0.5440000181198119, + -0.007999974250793684, + -0.6319999756813051, + -1.3039999847412114, + -2.0 + ], + 'name': 'TimeWarp2' + } + ] + } + } + + _check_expected_retimed_values( + "qt_timewarp.json", + expected_data, + handle_start=0, + handle_end=10, + ) + + + def test_img_sequence_no_handles(): """ Img sequence clip (no embedded timecode) @@ -449,3 +498,189 @@ def test_img_sequence_frozen_frame(): handle_start=10, handle_end=10, ) + + +def test_img_sequence_timewarp_beyond_range(): + """ + Img sequence clip + available files = 948674-948974 25fps + source_range = 909990.8339241028 + - 909995.8339241028 23.976fps + timewarp to get from 948845 to 948870 + """ + expected_data = { + 'mediaIn': 948845, + 'mediaOut': 948870, + 'handleStart': 0, + 'handleEnd': 10, + 'speed': 1.0, + 'versionData': {'handleEnd': 10, + 'handleStart': 0, + 'retime': True, + 'speed': 1.0, + 'timewarps': [ + { + 'Class': 'TimeWarp', + 'length': 1.0, + 'lookup': [ + -5.0, + -3.9440000305175777, + -2.852000034332275, + -1.6880000228881844, + -0.4160000076293944, + 1.0, + 2.5839999923706056, + 4.311999977111817, + 6.147999965667726, + 8.055999969482421, + 10.0 + ], + 'name': 'TimeWarp3' + } + ] + } + } + + _check_expected_retimed_values( + "img_seq_tw_beyond_range.json", + expected_data, + handle_start=0, + handle_end=10, + ) + + +def test_img_sequence_2X_speed_timewarp(): + """ + Img sequence clip + available files = 948674-948974 25fps + source_range = 909990.8339241028 + - 909995.8339241028 23.976fps + speed: 200% + timewarp to get from 948854 to 948874 + """ + expected_data = { + 'mediaIn': 948850, + 'mediaOut': 948874, + 'handleStart': 0, + 'handleEnd': 20, + 'speed': 2.0, + 'versionData': { + 'handleEnd': 20, + 'handleStart': 0, + 'retime': True, + 'speed': 2.0, + 'timewarps': [ + { + 'Class': 'TimeWarp', + 'length': 4.0, + 'lookup': [ + 2.0, + 1.7039999923706055, + 1.431999991416931, + 1.2079999942779531, + 1.055999998092652, + 1.0, + 1.056000007629395, + 1.208000022888184, + 1.432000034332276, + 1.7040000305175766, + 2.0 + ], + 'name': 'TimeWarp6' + } + ] + } + } + + _check_expected_retimed_values( + "img_seq_2x_time_warp.json", + expected_data, + handle_start=0, + handle_end=10, + ) + + +def test_img_sequence_multiple_timewarps(): + """ + Img sequence clip + available files = 948674-948974 25fps + source_range = 909990.8339241028 + - 909995.8339241028 23.976fps + multiple timewarps to get from 948842 to 948864 + """ + expected_data = { + 'mediaIn': 948845, + 'mediaOut': 948867, + 'handleStart': 0, + 'handleEnd': 10, + 'speed': 1.0, + 'versionData': { + 'handleEnd': 10, + 'handleStart': 0, + 'retime': True, + 'speed': 1.0, + 'timewarps': [ + { + 'Class': 'TimeWarp', + 'length': 1.0, + 'lookup': [ + -5.0, + -3.9440000305175777, + -2.852000034332275, + -1.6880000228881844, + -0.4160000076293944, + 1.0, + 2.5839999923706056, + 4.311999977111817, + 6.147999965667726, + 8.055999969482421, + 10.0 + ], + 'name': 'TimeWarp3' + }, + { + 'Class': 'TimeWarp', + 'length': 1.0, + 'lookup': [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + 'name': 'TimeWarp4' + }, + { + 'Class': 'TimeWarp', + 'length': 1.0, + 'lookup': [ + 0.0, + -1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -1.0 + ], + 'name': 'TimeWarp5' + } + ] + } + } + + _check_expected_retimed_values( + "img_seq_multiple_tws.json", + expected_data, + handle_start=0, + handle_end=10, + ) From 199ed55357fedcb81e84a32c4bc67ecf64478778 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 16 Jan 2025 15:48:02 +0800 Subject: [PATCH 445/546] add substance designer into OCIO and last workfile pre-launch hook --- client/ayon_core/hooks/pre_add_last_workfile_arg.py | 1 + client/ayon_core/hooks/pre_ocio_hook.py | 1 + client/ayon_core/pipeline/farm/pyblish_functions.py | 2 ++ 3 files changed, 4 insertions(+) diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index d5914c2352..daea8c5502 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -26,6 +26,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "photoshop", "tvpaint", "substancepainter", + "substancedesigner", "aftereffects", "wrap", "openrv", diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 7406aa42cf..78fc8c78de 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -10,6 +10,7 @@ class OCIOEnvHook(PreLaunchHook): order = 0 hosts = { "substancepainter", + "substancedesigner", "fusion", "blender", "aftereffects", diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index e48d99602e..16174a47a9 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -935,7 +935,9 @@ def _collect_expected_files_for_aov(files): ValueError: If there are multiple collections. """ + print(f"files: {files}") cols, rem = clique.assemble(files) + print(cols) # we shouldn't have any reminders. And if we do, it should # be just one item for single frame renders. if not cols and rem: From 03790a5f0c158f531315a806a2bf5a4dedbad0c1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 16 Jan 2025 23:39:26 +0100 Subject: [PATCH 446/546] Fix too big chunky icons in BorisFX Silhouette - This change was also tested in Fusion (where Qt runs completely separately and this addition didn't result in a visual difference there) --- client/ayon_core/style/style.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index bd96a3aeed..fa26605354 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -23,6 +23,9 @@ Enabled vs Disabled logic in most of stylesheets font-family: "Noto Sans"; font-weight: 450; outline: none; + + /* Fix icons in BorisFX Silhouette */ + icon-size: 16px; } QWidget { From 43f8764b7fb7d2481927f4bc55e91122765f28e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 17 Jan 2025 00:01:54 +0100 Subject: [PATCH 447/546] Reduce margins on Workfiles tool due to nested layouts --- client/ayon_core/tools/workfiles/widgets/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 8bcff66f50..1649a059cb 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -113,6 +113,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): main_layout = QtWidgets.QHBoxLayout(self) main_layout.addWidget(pages_widget, 1) + main_layout.setContentsMargins(0, 0, 0, 0) overlay_messages_widget = MessageOverlayObject(self) overlay_invalid_host = InvalidHostOverlay(self) From 17e20a2d0f007843f04a91873cbb7b1f11c323b2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 17 Jan 2025 00:07:14 +0100 Subject: [PATCH 448/546] Set the icon size in the stylesheet to avoid too big clunky icons in BorisFX Silhouette. The sizes appeared the same in Fusion and Maya with this added to the stylesheet (no changes there) --- client/ayon_core/style/style.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index bd96a3aeed..fa26605354 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -23,6 +23,9 @@ Enabled vs Disabled logic in most of stylesheets font-family: "Noto Sans"; font-weight: 450; outline: none; + + /* Fix icons in BorisFX Silhouette */ + icon-size: 16px; } QWidget { From e0133f54b66473aa8133e2759d6d95219e552eeb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Jan 2025 16:15:21 +0800 Subject: [PATCH 449/546] remove unrelated code --- client/ayon_core/pipeline/farm/pyblish_functions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 16174a47a9..e48d99602e 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -935,9 +935,7 @@ def _collect_expected_files_for_aov(files): ValueError: If there are multiple collections. """ - print(f"files: {files}") cols, rem = clique.assemble(files) - print(cols) # we shouldn't have any reminders. And if we do, it should # be just one item for single frame renders. if not cols and rem: From c718d0596f3612780de061ea3c7ce05596067a1d Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 20 Jan 2025 11:25:44 +0100 Subject: [PATCH 450/546] Enforce floating retime speed for Qt. --- client/ayon_core/pipeline/editorial.py | 9 +- .../publish/collect_otio_subset_resources.py | 15 +- .../resources/qt_reverse_speed_0_7.json | 160 ++++++++++++++++++ .../test_media_range_with_retimes.py | 29 ++++ 4 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_0_7.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index ed1cdf9974..7076b31ed9 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -406,7 +406,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): trim_range = otio.opentime.TimeRange( start_time=src_in, duration=retimed_duration, - ) # preserve discrete frame numbers @@ -479,16 +478,16 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "retime": True, "speed": time_scalar, "timewarps": time_warp_nodes, - "handleStart": int(handle_start), - "handleEnd": int(handle_end) + "handleStart": round(handle_start), + "handleEnd": round(handle_end) } } returning_dict = { "mediaIn": media_in_trimmed, "mediaOut": media_out_trimmed, - "handleStart": int(handle_start), - "handleEnd": int(handle_end), + "handleStart": round(handle_start), + "handleEnd": round(handle_end), "speed": time_scalar } diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index ed7e8ac4f1..bfcf5a71bb 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -6,6 +6,7 @@ Provides: instance -> otioReviewClips """ import os +import math import clique import pyblish.api @@ -69,9 +70,17 @@ class CollectOtioSubsetResources( self.log.debug( ">> retimed_attributes: {}".format(retimed_attributes)) - # break down into variables - media_in = int(retimed_attributes["mediaIn"]) - media_out = int(retimed_attributes["mediaOut"]) + # break down into variables as rounded frame numbers + # + # 0 1 2 3 4 + # |-------------|---------------|--------------|-------------| + # |_______________media range_______________| + # 0.6 3.2 + # + # As rounded frames, media_in = 0 and media_out = 4 + media_in = math.floor(retimed_attributes["mediaIn"]) + media_out = math.ceil(retimed_attributes["mediaOut"]) + handle_start = int(retimed_attributes["handleStart"]) handle_end = int(retimed_attributes["handleEnd"]) diff --git a/tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_0_7.json b/tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_0_7.json new file mode 100644 index 0000000000..3ed27bcf8b --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/qt_reverse_speed_0_7.json @@ -0,0 +1,160 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "sh010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 11.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 29.970030784606934 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "Speed", + "effect_name": "LinearTimeWarp", + "time_scalar": -0.699999988079071 + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "applieswhole": "1", + "hiero_source_type": "TrackItem", + "json_metadata": "{\"hiero_sub_products\": {\"io.ayon.creators.hiero.shot\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"shot\", \"productName\": \"shotMain\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.shot\", \"variant\": \"main\", \"folderPath\": \"/shots/hiero_qt_neg07x/sh010\", \"task\": null, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\", \"hierarchy\": \"shots/hiero_qt_neg07x\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg07x\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"clip_media\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_qt_neg07x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg07x\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"d7c96d32-6884-452f-9f8c-2383e20ca2db\", \"reviewTrack\": \"clip_media\", \"review\": true, \"folderName\": \"sh010\", \"label\": \"/shots/hiero_qt_neg07x/sh010 shotMain\", \"newHierarchyIntegration\": true, \"instance_id\": \"dae8823d-d664-4afd-9d9d-be20647ad756\", \"creator_attributes\": {\"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"frameStart\": 1001, \"frameEnd\": 1012, \"clipIn\": 0, \"clipOut\": 10, \"clipDuration\": 11, \"sourceOut\": 30.0, \"fps\": \"from_selection\", \"sourceIn\": 0}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}, \"io.ayon.creators.hiero.plate\": {\"id\": \"pyblish.avalon.instance\", \"productType\": \"plate\", \"productName\": \"plateVideo_1\", \"active\": true, \"creator_identifier\": \"io.ayon.creators.hiero.plate\", \"variant\": \"Video_1\", \"folderPath\": \"/shots/hiero_qt_neg07x/sh010\", \"task\": null, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\", \"hierarchy\": \"shots/hiero_qt_neg07x\", \"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg07x\", \"track\": \"Video_1\", \"shot\": \"sh010\", \"reviewableSource\": \"clip_media\", \"sourceResolution\": false, \"workfileFrameStart\": 1001, \"handleStart\": 10, \"handleEnd\": 10, \"parents\": [{\"entity_type\": \"folder\", \"folder_type\": \"folder\", \"entity_name\": \"shots\"}, {\"entity_type\": \"sequence\", \"folder_type\": \"sequence\", \"entity_name\": \"hiero_qt_neg07x\"}], \"hierarchyData\": {\"folder\": \"shots\", \"episode\": \"ep01\", \"sequence\": \"hiero_qt_neg07x\", \"track\": \"Video_1\"}, \"heroTrack\": true, \"uuid\": \"d7c96d32-6884-452f-9f8c-2383e20ca2db\", \"reviewTrack\": \"clip_media\", \"review\": true, \"folderName\": \"sh010\", \"parent_instance_id\": \"dae8823d-d664-4afd-9d9d-be20647ad756\", \"label\": \"/shots/hiero_qt_neg07x/sh010 plateVideo_1\", \"newHierarchyIntegration\": true, \"instance_id\": \"a1aa49c0-49a1-4499-a3ec-1ac35982d92b\", \"creator_attributes\": {\"parentInstance\": \"/shots/hiero_qt_neg07x/sh010 shotMain\", \"review\": true, \"reviewableSource\": \"clip_media\"}, \"publish_attributes\": {\"CollectSlackFamilies\": {\"additional_message\": \"\"}}}}, \"clip_index\": \"D812B65C-F1C7-DA48-9060-F932A50B2BB4\"}", + "label": "AYONdata_26480dbf", + "note": "AYON data container" + }, + "name": "AYONdata_26480dbf", + "color": "RED", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 23.976024627685547, + "value": 0.0 + } + } + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ExternalReference.1", + "metadata": { + "ayon.source.colorspace": "Gamma2.2", + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1920, + "clip.properties.blendfunc": "0", + "clip.properties.colourspacename": "error", + "clip.properties.domainroot": "", + "clip.properties.enabled": "1", + "clip.properties.expanded": "1", + "clip.properties.opacity": "1", + "clip.properties.valuesource": "", + "clip.properties.ycbcrmatrix": "0", + "com.apple.quicktime.codec": "H.264", + "foundry.source.audio": "", + "foundry.source.bitmapsize": "0", + "foundry.source.bitsperchannel": "0", + "foundry.source.channelformat": "integer", + "foundry.source.colourtransform": "Gamma2.2", + "foundry.source.duration": "101", + "foundry.source.filename": "qt_no_tc_24fps.mov", + "foundry.source.filesize": "", + "foundry.source.fragments": "1", + "foundry.source.framerate": "24", + "foundry.source.fullpath": "", + "foundry.source.height": "1080", + "foundry.source.layers": "colour", + "foundry.source.path": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov", + "foundry.source.pixelAspect": "1", + "foundry.source.pixelAspectRatio": "", + "foundry.source.pixelformat": "RGBA (Float32) Open Color IO space: 114", + "foundry.source.reelID": "", + "foundry.source.resolution": "", + "foundry.source.samplerate": "Invalid", + "foundry.source.shoottime": "4294967295", + "foundry.source.shortfilename": "qt_no_tc_24fps.mov", + "foundry.source.shot": "", + "foundry.source.shotDate": "", + "foundry.source.startTC": "", + "foundry.source.starttime": "0", + "foundry.source.timecode": "0", + "foundry.source.type": "QuickTime H.264", + "foundry.source.umid": "16634e88-6450-4727-6c6e-501f4b31b637", + "foundry.source.umidOriginator": "foundry.source.umid", + "foundry.source.width": "1920", + "foundry.timeline.autodiskcachemode": "Manual", + "foundry.timeline.colorSpace": "Gamma2.2", + "foundry.timeline.duration": "101", + "foundry.timeline.framerate": "24", + "foundry.timeline.outputformat": "", + "foundry.timeline.poster": "0", + "foundry.timeline.posterLayer": "colour", + "foundry.timeline.samplerate": "Invalid", + "media.input.bitsperchannel": "8-bit fixed", + "media.input.ctime": "2024-09-25 17:16:12", + "media.input.filename": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov", + "media.input.filereader": "mov64", + "media.input.filesize": "14631252", + "media.input.frame": "1", + "media.input.frame_rate": "24", + "media.input.height": "1080", + "media.input.mtime": "2024-09-25 17:16:16", + "media.input.pixel_aspect": "1", + "media.input.timecode": "00:00:00:00", + "media.input.width": "1920", + "media.quicktime.codec_id": "avc1", + "media.quicktime.codec_name": "h264", + "media.quicktime.encoder": "H.264", + "media.quicktime.nclc_matrix": "BT709", + "media.quicktime.nclc_primaries": "ITU-R BT.709", + "media.quicktime.nclc_transfer_function": "ITU-R BT.709", + "media.quicktime.thefoundry.Application": "Nuke", + "media.quicktime.thefoundry.ApplicationVersion": "15.0v5", + "media.quicktime.thefoundry.Colorspace": "Gamma2.2", + "media.quicktime.thefoundry.Writer": "mov64", + "media.quicktime.thefoundry.YCbCrMatrix": "Rec 709", + "media.stream.pixel_format": "yuv420p", + "uk.co.thefoundry.Application": "Nuke", + "uk.co.thefoundry.ApplicationVersion": "15.0v5", + "uk.co.thefoundry.Colorspace": "Gamma2.2", + "uk.co.thefoundry.Writer": "mov64", + "uk.co.thefoundry.YCbCrMatrix": "Rec 709" + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 24.0, + "value": 0.0 + } + }, + "available_image_bounds": null, + "target_url": "C:/Users/robin/OneDrive/Bureau/dev_ayon/data/qt_no_tc_24fps.mov" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 45cba64558..9ff2a7fdb2 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -146,6 +146,35 @@ def test_movie_reverse_speed_2x(): ) +def test_movie_reverse_speed_0_7x(): + """ + Movie clip (no timecode) + available files = 0-100 24fps + source_range = 29.97-40.97 23.976fps + speed = -0.7 + """ + expected_data = { + 'handleEnd': 7, + 'handleStart': 7, + 'mediaIn': 30.000000000000004, + 'mediaOut': 36.70769965924555, + 'speed': -0.699999988079071, + 'versionData': { + 'handleEnd': 7, + 'handleStart': 7, + 'retime': True, + 'speed': -0.699999988079071, + 'timewarps': [] + } + } + + _check_expected_retimed_values( + "qt_reverse_speed_0_7.json", + expected_data, + handle_start=10, + handle_end=10, + ) + def test_movie_frozen_frame(): """ From c166bf0514952bcef3e722d68f4c67cac47c0a92 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Jan 2025 21:55:34 +0800 Subject: [PATCH 451/546] add sbsar as the families --- client/ayon_core/plugins/publish/collect_resources_path.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_resources_path.py b/client/ayon_core/plugins/publish/collect_resources_path.py index 7a80d0054c..2e5b296228 100644 --- a/client/ayon_core/plugins/publish/collect_resources_path.py +++ b/client/ayon_core/plugins/publish/collect_resources_path.py @@ -66,7 +66,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "yeticacheUE", "tycache", "usd", - "oxrig" + "oxrig", + "sbsar", ] def process(self, instance): From 81bb74c7ea0e0c46a17f8bc16e54f0044fb81066 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 Jan 2025 18:07:18 +0100 Subject: [PATCH 452/546] Apply suggestions from code review Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/hooks/pre_add_last_workfile_arg.py | 2 +- client/ayon_core/hooks/pre_ocio_hook.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/hooks/pre_add_last_workfile_arg.py b/client/ayon_core/hooks/pre_add_last_workfile_arg.py index a931fb0cbe..5b10357632 100644 --- a/client/ayon_core/hooks/pre_add_last_workfile_arg.py +++ b/client/ayon_core/hooks/pre_add_last_workfile_arg.py @@ -30,7 +30,7 @@ class AddLastWorkfileToLaunchArgs(PreLaunchHook): "wrap", "openrv", "cinema4d", - "silhouette" + "silhouette", } launch_types = {LaunchTypes.local} diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index dd81cf053e..8765dbd0b2 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -21,7 +21,7 @@ class OCIOEnvHook(PreLaunchHook): "resolve", "openrv", "cinema4d", - "silhouette" + "silhouette", } launch_types = set() From 827651a4a669fefe8823db7095315d5ebb96599d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 21 Jan 2025 12:20:50 +0100 Subject: [PATCH 453/546] Update client/ayon_core/style/style.css Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/style/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index fa26605354..a5e54453cc 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -24,7 +24,7 @@ Enabled vs Disabled logic in most of stylesheets font-weight: 450; outline: none; - /* Fix icons in BorisFX Silhouette */ + /* Define icon size to fix size issues for most of DCCs */ icon-size: 16px; } From 41045c1092fce14032a09a2a4dd4608eddd8a71f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 21 Jan 2025 12:52:02 +0100 Subject: [PATCH 454/546] Add missing argument in docstring --- client/ayon_core/pipeline/staging_dir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/staging_dir.py b/client/ayon_core/pipeline/staging_dir.py index 2004096bd0..1cb2979415 100644 --- a/client/ayon_core/pipeline/staging_dir.py +++ b/client/ayon_core/pipeline/staging_dir.py @@ -158,6 +158,7 @@ def get_staging_dir_info( logger (Optional[logging.Logger]): Logger instance. prefix (Optional[str]) Optional prefix for staging dir name. suffix (Optional[str]): Optional suffix for staging dir name. + username (Optional[str]): AYON Username. Returns: Optional[StagingDir]: Staging dir info data From 2712260d08c9d8d777dfea9dbe7cfc4ec735f644 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 21 Jan 2025 14:49:22 +0000 Subject: [PATCH 455/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index b0ada09e7c..e90676d739 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.13+dev" +__version__ = "1.0.14" diff --git a/package.py b/package.py index 03b69d4c5c..bb38b431b1 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.13+dev" +version = "1.0.14" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 5e42aa7093..2496e3fa34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.13+dev" +version = "1.0.14" description = "" authors = ["Ynput Team "] readme = "README.md" From dec3dd2178130a0f197aeb4147c6eb1e290f84fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Jan 2025 14:50:15 +0000 Subject: [PATCH 456/546] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54f5d68b98..c0ab04abef 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,20 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.0.14 + - 1.0.13 + - 1.0.12 + - 1.0.11 + - 1.0.10 + - 1.0.9 + - 1.0.8 + - 1.0.7 + - 1.0.6 + - 1.0.5 + - 1.0.4 + - 1.0.3 + - 1.0.2 + - 1.0.1 - 1.0.0 - 0.4.4 - 0.4.3 From cdf7b743e8e8f38a3ccdf22ab0ec3ecb126e835d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:56:12 +0100 Subject: [PATCH 457/546] bump version to '1.0.15-dev' --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index e90676d739..2775cb606a 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.14" +__version__ = "1.0.15-dev" diff --git a/package.py b/package.py index bb38b431b1..af3342f3f2 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.14" +version = "1.0.15-dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 2496e3fa34..e040ce986f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.14" +version = "1.0.15-dev" description = "" authors = ["Ynput Team "] readme = "README.md" From bb2d38602e8abb9b83f923355145d9b7534a6cc8 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 21 Jan 2025 21:03:41 +0100 Subject: [PATCH 458/546] Restrict source range to available range. --- client/ayon_core/pipeline/editorial.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 7076b31ed9..933d1f1758 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -458,7 +458,10 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # adjust range if needed media_in_trimmed = min(media_in_trimmed, min(frame_range)) + media_in_trimmed = max(media_in_trimmed, media_in) + media_out_trimmed = max(media_out_trimmed, max(frame_range)) + media_out_trimmed = min(media_out_trimmed, media_out) # adjust available handles if needed if (media_in_trimmed - media_in) < handle_start: From 5f0aec7bbf1415b70a8729566f770187b873c897 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 22 Jan 2025 14:23:27 +0100 Subject: [PATCH 459/546] Rework tw computation. --- client/ayon_core/pipeline/editorial.py | 15 ++++++--------- .../editorial/test_media_range_with_retimes.py | 7 ++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 933d1f1758..02986753b7 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -431,11 +431,11 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): if time_warp_nodes: # Naive approach: Resolve consecutive timewarp(s) on range, # then check if plate range has to be extended beyond source range. - frame_range = [media_in_trimmed] - in_frame = media_in_trimmed + time_scalar - while in_frame <= media_out_trimmed: - frame_range.append(in_frame) + in_frame = media_in_trimmed + frame_range = [in_frame] + for _ in range(otio_clip.source_range.duration.to_frames() - 1): in_frame += time_scalar + frame_range.append(in_frame) for tw_idx, tw in enumerate(time_warp_nodes): for idx, frame_number in enumerate(frame_range): @@ -457,11 +457,8 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): frame_range[idx] = frame_range[new_idx] # adjust range if needed - media_in_trimmed = min(media_in_trimmed, min(frame_range)) - media_in_trimmed = max(media_in_trimmed, media_in) - - media_out_trimmed = max(media_out_trimmed, max(frame_range)) - media_out_trimmed = min(media_out_trimmed, media_out) + media_in_trimmed = max(min(frame_range), media_in) + media_out_trimmed = min(max(frame_range), media_out) # adjust available handles if needed if (media_in_trimmed - media_in) < handle_start: diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 9ff2a7fdb2..730e6b24cd 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -219,8 +219,8 @@ def test_movie_timewarp(): expected_data = { 'handleEnd': 10, 'handleStart': 0, - 'mediaIn': 948850, - 'mediaOut': 948860, + 'mediaIn': 948852, + 'mediaOut': 948858, 'speed': 1.0, 'versionData': {'handleEnd': 10, 'handleStart': 0, @@ -249,6 +249,7 @@ def test_movie_timewarp(): } } + import pdb ; pdb.set_trace() _check_expected_retimed_values( "qt_timewarp.json", expected_data, @@ -588,7 +589,7 @@ def test_img_sequence_2X_speed_timewarp(): timewarp to get from 948854 to 948874 """ expected_data = { - 'mediaIn': 948850, + 'mediaIn': 948854, 'mediaOut': 948874, 'handleStart': 0, 'handleEnd': 20, From 874abdb38c633cdea1887c0435802077a9cb8bd4 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 22 Jan 2025 15:36:25 +0100 Subject: [PATCH 460/546] Remove pdb. --- .../pipeline/editorial/test_media_range_with_retimes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index 730e6b24cd..a1d609b56e 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -249,7 +249,6 @@ def test_movie_timewarp(): } } - import pdb ; pdb.set_trace() _check_expected_retimed_values( "qt_timewarp.json", expected_data, From 2569015b9fe1a5b0baf5ee44539417ff0e678b3d Mon Sep 17 00:00:00 2001 From: Liam Hoflay Date: Wed, 22 Jan 2025 16:44:45 +0000 Subject: [PATCH 461/546] changing localhost for 0.0.0.0 in client/ayon_core/tools/tray/webserver/server.py --- client/ayon_core/tools/tray/webserver/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/tray/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py index d2a9b0fc6b..d9b3b857f5 100644 --- a/client/ayon_core/tools/tray/webserver/server.py +++ b/client/ayon_core/tools/tray/webserver/server.py @@ -28,7 +28,7 @@ def find_free_port( exclude_ports (list, tuple, set): List of ports that won't be checked form entered range. host (str): Host where will check for free ports. Set to - "localhost" by default. + "0.0.0.0" by default. """ if port_from is None: port_from = 8079 @@ -42,7 +42,7 @@ def find_free_port( # Default host is localhost but it is possible to look for other hosts if host is None: - host = "localhost" + host = "0.0.0.0" found_port = None while True: @@ -78,7 +78,7 @@ class WebServerManager: self._log = None self.port = port or 8079 - self.host = host or "localhost" + self.host = host or "0.0.0.0" self.on_stop_callbacks = [] From 66da9fa39a2f141895364031b4ad4ed7d4d02c3c Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 22 Jan 2025 20:27:55 +0100 Subject: [PATCH 462/546] Add tests to handle fractional speed. --- client/ayon_core/pipeline/editorial.py | 18 +- .../resources/img_seq_reverse_speed_0_7.json | 235 ++++++++++++++++++ .../test_media_range_with_retimes.py | 29 +++ 3 files changed, 274 insertions(+), 8 deletions(-) create mode 100644 tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_0_7.json diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 02986753b7..434fff0105 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -1,6 +1,7 @@ import os import re import clique +import math import opentimelineio as otio from opentimelineio import opentime as _ot @@ -397,12 +398,13 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): if is_input_sequence: src_in = conformed_source_range.start_time - src_duration = conformed_source_range.duration - + src_duration = math.ceil(otio_clip.source_range.duration.value * abs(time_scalar)) retimed_duration = otio.opentime.RationalTime( - src_duration.value * abs(time_scalar), - src_duration.rate + src_duration, + otio_clip.source_range.duration.rate ) + retimed_duration = retimed_duration.rescaled_to(src_in.rate) + trim_range = otio.opentime.TimeRange( start_time=src_in, duration=retimed_duration, @@ -478,16 +480,16 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): "retime": True, "speed": time_scalar, "timewarps": time_warp_nodes, - "handleStart": round(handle_start), - "handleEnd": round(handle_end) + "handleStart": math.ceil(handle_start), + "handleEnd": math.ceil(handle_end) } } returning_dict = { "mediaIn": media_in_trimmed, "mediaOut": media_out_trimmed, - "handleStart": round(handle_start), - "handleEnd": round(handle_end), + "handleStart": math.ceil(handle_start), + "handleEnd": math.ceil(handle_end), "speed": time_scalar } diff --git a/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_0_7.json b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_0_7.json new file mode 100644 index 0000000000..0939161817 --- /dev/null +++ b/tests/client/ayon_core/pipeline/editorial/resources/img_seq_reverse_speed_0_7.json @@ -0,0 +1,235 @@ +{ + "OTIO_SCHEMA": "Clip.2", + "metadata": {}, + "name": "img_seq_revsh0010", + "source_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 41.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1040.0 + } + }, + "effects": [ + { + "OTIO_SCHEMA": "LinearTimeWarp.1", + "metadata": {}, + "name": "Speed", + "effect_name": "LinearTimeWarp", + "time_scalar": -0.7 + } + ], + "markers": [ + { + "OTIO_SCHEMA": "Marker.2", + "metadata": { + "clip_index": "e7fede03-d769-4827-a014-35b50170e914", + "flame_sub_products": { + "io.ayon.creators.flame.plate": { + "active": true, + "clipName": "{sequence}{shot}", + "clipRename": true, + "clipVariant": "", + "clip_index": "e7fede03-d769-4827-a014-35b50170e914", + "countFrom": 10, + "countSteps": 10, + "creator_attributes": { + "parentInstance": "/test_robin/img_seq_rev/img_seq_revsh0010 shot", + "review": false, + "reviewableSource": "clip_media" + }, + "creator_identifier": "io.ayon.creators.flame.plate", + "episode": "ep01", + "export_audio": false, + "folder": "test_robin", + "folderName": "img_seq_revsh0010", + "folderPath": "/test_robin/img_seq_rev/img_seq_revsh0010", + "handleEnd": 5, + "handleStart": 5, + "heroTrack": true, + "hierarchy": "test_robin/img_seq_rev", + "hierarchyData": { + "episode": "ep01", + "folder": "test_robin", + "sequence": "img_seq_rev", + "track": "noname1" + }, + "id": "pyblish.avalon.instance", + "includeHandles": false, + "instance_id": "a06107cd-49ad-48cb-a84f-67f53dfd58ef", + "label": "/test_robin/img_seq_rev/img_seq_revsh0010 plateNoname1", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parent_instance_id": "896c2dad-03a6-4a18-97f5-ecf8f00a6180", + "parents": [ + { + "entity_name": "test_robin", + "folder_type": "folder" + }, + { + "entity_name": "img_seq_rev", + "folder_type": "sequence" + } + ], + "productName": "plateNoname1", + "productType": "plate", + "publish": true, + "publish_attributes": {}, + "retimedFramerange": true, + "retimedHandles": true, + "reviewTrack": null, + "reviewableSource": null, + "segmentIndex": true, + "sequence": "img_seq_rev", + "shot": "sh####", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "useShotName": false, + "use_selection": true, + "vSyncOn": false, + "vSyncTrack": "*", + "variant": "noname1", + "workfileFrameStart": 1001 + }, + "io.ayon.creators.flame.shot": { + "active": true, + "clipName": "{sequence}{shot}", + "clipRename": true, + "clipVariant": "", + "clip_index": "e7fede03-d769-4827-a014-35b50170e914", + "countFrom": 10, + "countSteps": 10, + "creator_attributes": { + "clipDuration": 41, + "clipIn": 1, + "clipOut": 41, + "fps": "from_selection", + "frameEnd": 1042, + "frameStart": 1001, + "handleEnd": 5, + "handleStart": 5, + "includeHandles": false, + "retimedFramerange": true, + "retimedHandles": true, + "sourceIn": 1068, + "sourceOut": 1040, + "workfileFrameStart": 1001 + }, + "creator_identifier": "io.ayon.creators.flame.shot", + "episode": "ep01", + "export_audio": false, + "folder": "test_robin", + "folderName": "img_seq_revsh0010", + "folderPath": "/test_robin/img_seq_rev/img_seq_revsh0010", + "handleEnd": 5, + "handleStart": 5, + "heroTrack": true, + "hierarchy": "test_robin/img_seq_rev", + "hierarchyData": { + "episode": "ep01", + "folder": "test_robin", + "sequence": "img_seq_rev", + "track": "noname1" + }, + "id": "pyblish.avalon.instance", + "includeHandles": false, + "instance_id": "896c2dad-03a6-4a18-97f5-ecf8f00a6180", + "label": "/test_robin/img_seq_rev/img_seq_revsh0010 shot", + "newAssetPublishing": true, + "newHierarchyIntegration": true, + "parents": [ + { + "entity_name": "test_robin", + "folder_type": "folder" + }, + { + "entity_name": "img_seq_rev", + "folder_type": "sequence" + } + ], + "productName": "shotMain", + "productType": "shot", + "publish": true, + "publish_attributes": {}, + "retimedFramerange": true, + "retimedHandles": true, + "reviewTrack": null, + "reviewableSource": null, + "segmentIndex": true, + "sequence": "img_seq_rev", + "shot": "sh####", + "sourceResolution": false, + "task": null, + "track": "{_track_}", + "useShotName": false, + "use_selection": true, + "vSyncOn": false, + "vSyncTrack": "*", + "variant": "main", + "workfileFrameStart": 1001 + } + }, + "publish": true + }, + "name": "AYONData", + "color": "CYAN", + "marked_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 22.0 + } + }, + "comment": "" + } + ], + "enabled": true, + "media_references": { + "DEFAULT_MEDIA": { + "OTIO_SCHEMA": "ImageSequenceReference.1", + "metadata": { + "ayon.source.height": 1080, + "ayon.source.pixelAspect": 1.0, + "ayon.source.width": 1920, + "isSequence": true, + "padding": 4 + }, + "name": "", + "available_range": { + "OTIO_SCHEMA": "TimeRange.1", + "duration": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 101.0 + }, + "start_time": { + "OTIO_SCHEMA": "RationalTime.1", + "rate": 25.0, + "value": 1000.0 + } + }, + "available_image_bounds": null, + "target_url_base": "/home/ynput/CODE/testing_flame/test_data/sample_media_robin/Samples media/img_sequence/tif/", + "name_prefix": "output.", + "name_suffix": ".tif", + "start_frame": 1000, + "frame_step": 1, + "rate": 25.0, + "frame_zero_padding": 4, + "missing_frame_policy": "error" + } + }, + "active_media_reference_key": "DEFAULT_MEDIA" +} \ No newline at end of file diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index a1d609b56e..ab67d49e22 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -438,6 +438,35 @@ def test_img_sequence_reverse_speed_from_24_to_23_976fps(): ) +def test_img_sequence_reverse_speed_0_7(): + """ + Img sequence clip + available files = 1000-1100 24fps + source_range = 1040-1081 25fps + """ + expected_data = { + 'mediaIn': 1040, + 'mediaOut': 1068, + 'handleStart': 4, + 'handleEnd': 4, + 'speed': -0.7, + 'versionData': { + 'retime': True, + 'speed': -0.7, + 'timewarps': [], + 'handleStart': 4, + 'handleEnd': 4 + } + } + + _check_expected_retimed_values( + "img_seq_reverse_speed_0_7.json", + expected_data, + handle_start=5, + handle_end=5, + ) + + def test_img_sequence_2x_speed(): """ Img sequence clip From 133bb32c8154234761b6e798528d41a121f275ea Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 22 Jan 2025 20:52:15 +0100 Subject: [PATCH 463/546] Fix lint. --- client/ayon_core/pipeline/editorial.py | 30 ++++++++++++++----- .../test_media_range_with_retimes.py | 10 +++---- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 434fff0105..2ecc786581 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -398,7 +398,10 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): if is_input_sequence: src_in = conformed_source_range.start_time - src_duration = math.ceil(otio_clip.source_range.duration.value * abs(time_scalar)) + src_duration = math.ceil( + otio_clip.source_range.duration.value + * abs(time_scalar) + ) retimed_duration = otio.opentime.RationalTime( src_duration, otio_clip.source_range.duration.rate @@ -422,9 +425,15 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # compute retimed range media_in_trimmed = conformed_source_range.start_time.value + offset_in - offset_duration = conformed_source_range.duration.value * abs(time_scalar) - offset_duration -= 1 # duration 1 frame -> freeze frame -> end = start + 0 - offset_duration = max(0, offset_duration) # negative duration = frozen frame + offset_duration = ( + conformed_source_range.duration.value + * abs(time_scalar) + ) + + # duration 1 frame -> freeze frame -> end = start + 0 + offset_duration -= 1 + # negative duration = frozen frame + offset_duration = max(0, offset_duration) media_out_trimmed = media_in_trimmed + offset_duration media_in = available_range.start_time.value @@ -443,17 +452,22 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): for idx, frame_number in enumerate(frame_range): # First timewarp, apply on media range if tw_idx == 0: - frame_range[idx] = round(frame_number + tw["lookup"][idx] * time_scalar) + frame_range[idx] = round( + frame_number + + (tw["lookup"][idx] * time_scalar) + ) # Consecutive timewarp, apply on the previous result else: new_idx = round(idx + tw["lookup"][idx]) if not 0 <= new_idx < len(frame_range): # TODO: implementing this would need to actually have - # retiming engine resolve process within AYON, resolving wraps - # as curves, then projecting those into the previous media_range. + # retiming engine resolve process within AYON, + # resolving wraps as curves, then projecting + # those into the previous media_range. raise NotImplementedError( - "Unsupported consecutive timewarps (out of computed range)" + "Unsupported consecutive timewarps " + "(out of computed range)" ) frame_range[idx] = frame_range[new_idx] diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index ab67d49e22..fbab60623f 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -501,7 +501,7 @@ def test_img_sequence_2x_speed_resolve(): """ Img sequence clip available files = 0-99 24fps - source_range = 38-49 24fps + source_range = 38-49 24fps speed = 2.0 """ expected_data = { @@ -531,7 +531,7 @@ def test_img_sequence_frozen_frame(): """ Img sequence clip available files = 948674-948974 25fps - source_range = 909990.8339241028 + source_range = 909990.8339241028 - 909995.8339241028 23.976fps speed = 0.0 """ @@ -562,7 +562,7 @@ def test_img_sequence_timewarp_beyond_range(): """ Img sequence clip available files = 948674-948974 25fps - source_range = 909990.8339241028 + source_range = 909990.8339241028 - 909995.8339241028 23.976fps timewarp to get from 948845 to 948870 """ @@ -611,7 +611,7 @@ def test_img_sequence_2X_speed_timewarp(): """ Img sequence clip available files = 948674-948974 25fps - source_range = 909990.8339241028 + source_range = 909990.8339241028 - 909995.8339241028 23.976fps speed: 200% timewarp to get from 948854 to 948874 @@ -662,7 +662,7 @@ def test_img_sequence_multiple_timewarps(): """ Img sequence clip available files = 948674-948974 25fps - source_range = 909990.8339241028 + source_range = 909990.8339241028 - 909995.8339241028 23.976fps multiple timewarps to get from 948842 to 948864 """ From 8dc243f2fef0e523bf639b2ccf53539eb7c487bb Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 22 Jan 2025 21:03:41 +0100 Subject: [PATCH 464/546] Address feedback from PR. --- client/ayon_core/pipeline/editorial.py | 29 ++++++++++--------- .../publish/collect_otio_subset_resources.py | 6 ++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 2ecc786581..fc962300d8 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -430,10 +430,10 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): * abs(time_scalar) ) - # duration 1 frame -> freeze frame -> end = start + 0 - offset_duration -= 1 - # negative duration = frozen frame - offset_duration = max(0, offset_duration) + # Offset duration by 1 for media out frame + # - only if duration is not single frame (start frame != end frame) + if offset_duration > 0: + offset_duration -= 1 media_out_trimmed = media_in_trimmed + offset_duration media_in = available_range.start_time.value @@ -460,17 +460,18 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): else: new_idx = round(idx + tw["lookup"][idx]) - if not 0 <= new_idx < len(frame_range): - # TODO: implementing this would need to actually have - # retiming engine resolve process within AYON, - # resolving wraps as curves, then projecting - # those into the previous media_range. - raise NotImplementedError( - "Unsupported consecutive timewarps " - "(out of computed range)" - ) + if 0 <= new_idx < len(frame_range): + frame_range[idx] = frame_range[new_idx] + continue - frame_range[idx] = frame_range[new_idx] + # TODO: implementing this would need to actually have + # retiming engine resolve process within AYON, + # resolving wraps as curves, then projecting + # those into the previous media_range. + raise NotImplementedError( + "Unsupported consecutive timewarps " + "(out of computed range)" + ) # adjust range if needed media_in_trimmed = max(min(frame_range), media_in) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index bfcf5a71bb..deb51f62a5 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -188,7 +188,7 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, collection=collection) else: - filename, = tuple(collection) + filename = tuple(collection)[0] self.log.debug(filename) # TODO: discuss this, it erases frame number. @@ -200,8 +200,8 @@ class CollectOtioSubsetResources( and "review" in instance.data["families"] ): review_repre = self._create_representation( - frame_start, frame_end, collection=collection, - delete=True, review=True) + frame_start, frame_end, collection=collection, + delete=True, review=True) else: _trim = False From b2bbec42ba2d068076d7526e96e84e9c47ae839e Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 22 Jan 2025 21:04:54 +0100 Subject: [PATCH 465/546] Fix lint. --- client/ayon_core/pipeline/editorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index fc962300d8..6e5e2ec67a 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -432,7 +432,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # Offset duration by 1 for media out frame # - only if duration is not single frame (start frame != end frame) - if offset_duration > 0: + if offset_duration > 0: offset_duration -= 1 media_out_trimmed = media_in_trimmed + offset_duration From 27145add3aefa23bf34040f7e307307bd473fd00 Mon Sep 17 00:00:00 2001 From: Liam Hoflay Date: Thu, 23 Jan 2025 10:25:13 +0000 Subject: [PATCH 466/546] making 127.0.0.1 rather than 0.0.0.0 --- client/ayon_core/tools/tray/webserver/server.py | 6 +++--- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/tray/webserver/server.py b/client/ayon_core/tools/tray/webserver/server.py index d9b3b857f5..70d1fc8c0f 100644 --- a/client/ayon_core/tools/tray/webserver/server.py +++ b/client/ayon_core/tools/tray/webserver/server.py @@ -28,7 +28,7 @@ def find_free_port( exclude_ports (list, tuple, set): List of ports that won't be checked form entered range. host (str): Host where will check for free ports. Set to - "0.0.0.0" by default. + "127.0.0.1" by default. """ if port_from is None: port_from = 8079 @@ -42,7 +42,7 @@ def find_free_port( # Default host is localhost but it is possible to look for other hosts if host is None: - host = "0.0.0.0" + host = "127.0.0.1" found_port = None while True: @@ -78,7 +78,7 @@ class WebServerManager: self._log = None self.port = port or 8079 - self.host = host or "0.0.0.0" + self.host = host or "127.0.0.1" self.on_stop_callbacks = [] diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 2775cb606a..fabbb4f0e9 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.15-dev" +__version__ = "1.0.14-bk.1" diff --git a/package.py b/package.py index af3342f3f2..db5e0b80dc 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.15-dev" +version = "1.0.14-bk.1" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e040ce986f..dc550a1eda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.15-dev" +version = "1.0.14-bk.1" description = "" authors = ["Ynput Team "] readme = "README.md" From 6708abaa43c1e22ea2435f450c03ca5c2e6cd500 Mon Sep 17 00:00:00 2001 From: Liam Hoflay Date: Thu, 23 Jan 2025 11:19:48 +0000 Subject: [PATCH 467/546] version revert --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index fabbb4f0e9..2775cb606a 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.14-bk.1" +__version__ = "1.0.15-dev" diff --git a/package.py b/package.py index db5e0b80dc..af3342f3f2 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.14-bk.1" +version = "1.0.15-dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index dc550a1eda..e040ce986f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.14-bk.1" +version = "1.0.15-dev" description = "" authors = ["Ynput Team "] readme = "README.md" From ffec35f69f140a426ee3198ef9de6d01a7c11840 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 23 Jan 2025 17:26:32 +0100 Subject: [PATCH 468/546] Add Silhouette host to defaults of few validators --- server/settings/publish_plugins.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 1bf2e853cf..2cca9cd66e 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1033,7 +1033,8 @@ DEFAULT_PUBLISH_VALUES = { "maya", "nuke", "photoshop", - "substancepainter" + "substancepainter", + "silhouette" ], "enabled": True, "optional": False, @@ -1053,7 +1054,8 @@ DEFAULT_PUBLISH_VALUES = { "harmony", "photoshop", "aftereffects", - "fusion" + "fusion", + "silhouette" ], "enabled": True, "optional": True, From 375378e74cc7cddb1d4e83bbc90c1bfd14d52693 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:18:27 +0100 Subject: [PATCH 469/546] auto-fill project entity if is not passed in --- client/ayon_core/pipeline/create/creator_plugins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 445b41cb4b..ce109e3a37 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -562,6 +562,9 @@ class BaseCreator(ABC): instance ) + if not project_entity: + project_entity = self.create_context.get_current_project_entity() + return get_product_name( project_name, task_name, From 8b4f5ec42a210c08f22bba61abc09d1d0f9dd047 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:21:22 +0100 Subject: [PATCH 470/546] added one more check if current project is the project name passed in --- client/ayon_core/pipeline/create/creator_plugins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index ce109e3a37..fca671d546 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -562,7 +562,8 @@ class BaseCreator(ABC): instance ) - if not project_entity: + cur_project_name = self.create_context.get_current_project_name() + if not project_entity and project_name == cur_project_name: project_entity = self.create_context.get_current_project_entity() return get_product_name( From 509c6a84d3004882cdff9b45e8cb31b18465db7e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:24:02 +0100 Subject: [PATCH 471/546] set minimum size of content widget to be able to stretch it's size --- client/ayon_core/tools/publisher/widgets/report_page.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index b7afcf470a..2684f85356 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -1332,6 +1332,8 @@ class InstancesLogsView(QtWidgets.QFrame): content_widget = QtWidgets.QWidget(content_wrap_widget) content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_widget.setMinimumSize(80, 80) + content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(8, 8, 8, 8) content_layout.setSpacing(15) From c2f3d8b114ecd93871029144bc81a77ad841758e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Jan 2025 14:31:06 +0100 Subject: [PATCH 472/546] use 'AYON_INSTANCE_ID' by default instead of 'AVALON_' --- client/ayon_core/pipeline/create/legacy_create.py | 4 ++-- client/ayon_core/pipeline/create/structures.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/legacy_create.py b/client/ayon_core/pipeline/create/legacy_create.py index ec9b23ac62..f6427d9bd1 100644 --- a/client/ayon_core/pipeline/create/legacy_create.py +++ b/client/ayon_core/pipeline/create/legacy_create.py @@ -9,7 +9,7 @@ import os import logging import collections -from ayon_core.pipeline.constants import AVALON_INSTANCE_ID +from ayon_core.pipeline.constants import AYON_INSTANCE_ID from .product_name import get_product_name @@ -34,7 +34,7 @@ class LegacyCreator: # Default data self.data = collections.OrderedDict() # TODO use 'AYON_INSTANCE_ID' when all hosts support it - self.data["id"] = AVALON_INSTANCE_ID + self.data["id"] = AYON_INSTANCE_ID self.data["productType"] = self.product_type self.data["folderPath"] = folder_path self.data["productName"] = name diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index a1a4d5f8ef..a45e053cca 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -492,7 +492,7 @@ class CreatedInstance: item_id = data.get("id") # TODO use only 'AYON_INSTANCE_ID' when all hosts support it if item_id not in {AYON_INSTANCE_ID, AVALON_INSTANCE_ID}: - item_id = AVALON_INSTANCE_ID + item_id = AYON_INSTANCE_ID self._data["id"] = item_id self._data["productType"] = product_type self._data["productName"] = product_name From c380ebfedf03e77e4744df42db75eb527ae7663f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:09:03 +0100 Subject: [PATCH 473/546] use 'QTextEdit' for log message --- client/ayon_core/style/style.css | 2 + .../tools/publisher/widgets/report_page.py | 58 ++++++++++++++++--- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/style/style.css b/client/ayon_core/style/style.css index a5e54453cc..0e19702d53 100644 --- a/client/ayon_core/style/style.css +++ b/client/ayon_core/style/style.css @@ -1171,6 +1171,8 @@ ValidationArtistMessage QLabel { #PublishLogMessage { font-family: "Noto Sans Mono"; + border: none; + padding: 0; } #PublishInstanceLogsLabel { diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index 2684f85356..58c78c0b06 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -1117,6 +1117,54 @@ class LogIconFrame(QtWidgets.QFrame): painter.end() +class LogItemMessage(QtWidgets.QTextEdit): + def __init__(self, msg, parent): + super().__init__(msg, parent) + + self.setObjectName("PublishLogMessage") + self.setReadOnly(True) + self.setFrameStyle(QtWidgets.QFrame.NoFrame) + self.setLineWidth(0) + self.setMidLineWidth(0) + pal = self.palette() + pal.setColor(QtGui.QPalette.Base, QtCore.Qt.transparent) + self.setPalette(pal) + self.setContentsMargins(0, 0, 0, 0) + viewport = self.viewport() + viewport.setContentsMargins(0, 0, 0, 0) + + self.setTextInteractionFlags( + QtCore.Qt.TextBrowserInteraction) + self.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) + self.setLineWrapMode(QtWidgets.QTextEdit.WidgetWidth) + self.setWordWrapMode( + QtGui.QTextOption.WrapMode.WrapAtWordBoundaryOrAnywhere + ) + self.setSizePolicy( + QtWidgets.QSizePolicy.Preferred, + QtWidgets.QSizePolicy.Maximum + ) + document = self.document() + document.documentLayout().documentSizeChanged.connect( + self._adjust_minimum_size + ) + document.setDocumentMargin(0.0) + self._height = None + + def _adjust_minimum_size(self, size): + self._height = size.height() + (2 * self.frameWidth()) + self.updateGeometry() + + def sizeHint(self): + size = super().sizeHint() + if self._height is not None: + size.setHeight(self._height) + return size + + def minimumSizeHint(self): + return self.sizeHint() + + class LogItemWidget(QtWidgets.QWidget): log_level_to_flag = { 10: LOG_DEBUG_VISIBLE, @@ -1132,12 +1180,7 @@ class LogItemWidget(QtWidgets.QWidget): type_flag, level_n = self._get_log_info(log) icon_label = LogIconFrame( self, log["type"], level_n, log.get("is_validation_error")) - message_label = QtWidgets.QLabel(log["msg"].rstrip(), self) - message_label.setObjectName("PublishLogMessage") - message_label.setTextInteractionFlags( - QtCore.Qt.TextBrowserInteraction) - message_label.setCursor(QtGui.QCursor(QtCore.Qt.IBeamCursor)) - message_label.setWordWrap(True) + message_label = LogItemMessage(log["msg"].rstrip(), self) main_layout = QtWidgets.QHBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) @@ -1290,6 +1333,7 @@ class InstanceLogsWidget(QtWidgets.QWidget): label_widget = QtWidgets.QLabel(instance.label, self) label_widget.setObjectName("PublishInstanceLogsLabel") + label_widget.setWordWrap(True) logs_grid = LogsWithIconsView(instance.logs, self) layout = QtWidgets.QVBoxLayout(self) @@ -1329,10 +1373,10 @@ class InstancesLogsView(QtWidgets.QFrame): content_wrap_widget = QtWidgets.QWidget(scroll_area) content_wrap_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) + content_wrap_widget.setMinimumWidth(80) content_widget = QtWidgets.QWidget(content_wrap_widget) content_widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - content_widget.setMinimumSize(80, 80) content_layout = QtWidgets.QVBoxLayout(content_widget) content_layout.setContentsMargins(8, 8, 8, 8) From 22546fd9c695a5e02a61252414a153b76ee9921a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:10:33 +0100 Subject: [PATCH 474/546] remove whitespaces --- client/ayon_core/tools/publisher/widgets/report_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index 58c78c0b06..c0c1120fc9 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -1164,7 +1164,7 @@ class LogItemMessage(QtWidgets.QTextEdit): def minimumSizeHint(self): return self.sizeHint() - + class LogItemWidget(QtWidgets.QWidget): log_level_to_flag = { 10: LOG_DEBUG_VISIBLE, From aadd107975137ae4fcc611e93057bc2532850bb5 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 27 Jan 2025 13:04:00 +0100 Subject: [PATCH 475/546] Remove unecessary offsets. --- client/ayon_core/pipeline/editorial.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 6e5e2ec67a..e6e6294a81 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -344,8 +344,6 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # modifiers time_scalar = 1. - offset_in = 0 - offset_out = 0 time_warp_nodes = [] # Check for speed effects and adjust playback speed accordingly @@ -379,17 +377,12 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): # add to timewarp nodes time_warp_nodes.append(tw_node) - # multiply by time scalar - offset_in *= time_scalar - offset_out *= time_scalar - # scale handles handle_start *= abs(time_scalar) handle_end *= abs(time_scalar) # flip offset and handles if reversed speed if time_scalar < 0: - offset_in, offset_out = offset_out, offset_in handle_start, handle_end = handle_end, handle_start # If media source is an image sequence, returned @@ -423,7 +416,7 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): else: # compute retimed range - media_in_trimmed = conformed_source_range.start_time.value + offset_in + media_in_trimmed = conformed_source_range.start_time.value offset_duration = ( conformed_source_range.duration.value From 521e50619ecda3e8001b1280c2e46952d8c3a2de Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Jan 2025 13:55:52 +0100 Subject: [PATCH 476/546] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- server/settings/publish_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 2cca9cd66e..18e7d67f90 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1034,7 +1034,7 @@ DEFAULT_PUBLISH_VALUES = { "nuke", "photoshop", "substancepainter", - "silhouette" + "silhouette", ], "enabled": True, "optional": False, @@ -1055,7 +1055,7 @@ DEFAULT_PUBLISH_VALUES = { "photoshop", "aftereffects", "fusion", - "silhouette" + "silhouette", ], "enabled": True, "optional": True, From 83b7c3d4429244126d984dc20bc7bcf5a663ab3e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 14:00:54 +0100 Subject: [PATCH 477/546] fix new-line character --- client/ayon_core/tools/publisher/widgets/report_page.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/widgets/report_page.py b/client/ayon_core/tools/publisher/widgets/report_page.py index c0c1120fc9..1e46e7e52c 100644 --- a/client/ayon_core/tools/publisher/widgets/report_page.py +++ b/client/ayon_core/tools/publisher/widgets/report_page.py @@ -1119,7 +1119,10 @@ class LogIconFrame(QtWidgets.QFrame): class LogItemMessage(QtWidgets.QTextEdit): def __init__(self, msg, parent): - super().__init__(msg, parent) + super().__init__(parent) + + # Set as plain text to propagate new line characters + self.setPlainText(msg) self.setObjectName("PublishLogMessage") self.setReadOnly(True) From 3782105fc0c7b45edc11b32f3a09f26c2d542347 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:36:17 +0100 Subject: [PATCH 478/546] EnumDef allows placeholder to be set --- client/ayon_core/lib/attribute_definitions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e8327a45b6..e750c00a27 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -554,12 +554,18 @@ class EnumDef(AbstractAttrDef): """ type = "enum" + type_attributes = [ + "multiselection", + "placeholder", + ] + def __init__( self, key: str, items: "EnumItemsInputType", default: "Union[str, List[Any]]" = None, multiselection: Optional[bool] = False, + placeholder: Optional[str] = None, **kwargs ): if not items: @@ -587,6 +593,7 @@ class EnumDef(AbstractAttrDef): self.items: List["EnumItemDict"] = items self._item_values: Set[Any] = item_values_set self.multiselection: bool = multiselection + self.placeholder: Optional[str] = placeholder def convert_value(self, value): if not self.multiselection: @@ -612,7 +619,6 @@ class EnumDef(AbstractAttrDef): def serialize(self): data = super().serialize() data["items"] = copy.deepcopy(self.items) - data["multiselection"] = self.multiselection return data @staticmethod From 29dbc8b8cf50cd72825d61964d17e601733c47d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:37:14 +0100 Subject: [PATCH 479/546] set the placeholder on widgets --- client/ayon_core/tools/attribute_defs/widgets.py | 6 +++++- .../tools/utils/multiselection_combobox.py | 4 ++-- client/ayon_core/tools/utils/widgets.py | 13 ++++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 93f63730f5..635855863c 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -640,11 +640,15 @@ class EnumAttrWidget(_BaseAttrDefWidget): return self.attr_def.multiselection def _ui_init(self): + placeholder = self.attr_def.placeholder if self.multiselection: - input_widget = MultiSelectionComboBox(self) + input_widget = MultiSelectionComboBox( + self, placeholder=placeholder + ) else: input_widget = CustomTextComboBox(self) + input_widget.set_placeholder(placeholder) combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) input_widget.setItemDelegate(combo_delegate) self._combo_delegate = combo_delegate diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index 34361fca17..b90838267b 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -61,7 +61,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): self._block_mouse_release_timer = QtCore.QTimer(self, singleShot=True) self._initial_mouse_pos = None self._separator = separator - self._placeholder_text = placeholder + self._placeholder_text = placeholder or "" delegate = ComboItemDelegate(self) self.setItemDelegate(delegate) @@ -74,7 +74,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): return self._placeholder_text def set_placeholder_text(self, text): - self._placeholder_text = text + self._placeholder_text = text or "" self._update_size_hint() def set_custom_text(self, text): diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 4c2b418c41..8d5a11b811 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -54,7 +54,7 @@ class ComboBox(QtWidgets.QComboBox): """ def __init__(self, *args, **kwargs): - super(ComboBox, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) delegate = QtWidgets.QStyledItemDelegate() self.setItemDelegate(delegate) self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -63,7 +63,7 @@ class ComboBox(QtWidgets.QComboBox): def wheelEvent(self, event): if self.hasFocus(): - return super(ComboBox, self).wheelEvent(event) + return super().wheelEvent(event) class CustomTextComboBox(ComboBox): @@ -71,7 +71,14 @@ class CustomTextComboBox(ComboBox): def __init__(self, *args, **kwargs): self._custom_text = None - super(CustomTextComboBox, self).__init__(*args, **kwargs) + self._placeholder = placeholder + super().__init__(*args, **kwargs) + + def set_placeholder(self, placeholder: Optional[str]): + if placeholder == self._placeholder: + return + self.lineEdit().setPlaceholderText(placeholder) + self.repaint() def set_custom_text(self, text=None): if self._custom_text != text: From 4c7176c7521827c4f48f2694ba3337d2fe0bbcf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:40:03 +0100 Subject: [PATCH 480/546] remove placeholder from single selection combobox --- client/ayon_core/tools/attribute_defs/widgets.py | 4 +--- client/ayon_core/tools/utils/widgets.py | 7 ------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 635855863c..201fd5be48 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -640,15 +640,13 @@ class EnumAttrWidget(_BaseAttrDefWidget): return self.attr_def.multiselection def _ui_init(self): - placeholder = self.attr_def.placeholder if self.multiselection: input_widget = MultiSelectionComboBox( - self, placeholder=placeholder + self, placeholder=self.attr_def.placeholder ) else: input_widget = CustomTextComboBox(self) - input_widget.set_placeholder(placeholder) combo_delegate = QtWidgets.QStyledItemDelegate(input_widget) input_widget.setItemDelegate(combo_delegate) self._combo_delegate = combo_delegate diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 8d5a11b811..059a06648b 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -71,15 +71,8 @@ class CustomTextComboBox(ComboBox): def __init__(self, *args, **kwargs): self._custom_text = None - self._placeholder = placeholder super().__init__(*args, **kwargs) - def set_placeholder(self, placeholder: Optional[str]): - if placeholder == self._placeholder: - return - self.lineEdit().setPlaceholderText(placeholder) - self.repaint() - def set_custom_text(self, text=None): if self._custom_text != text: self._custom_text = text From a312c463842bbeae1053054503f80168faa7abd3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:40:14 +0100 Subject: [PATCH 481/546] added placeholder to docstring --- client/ayon_core/lib/attribute_definitions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e750c00a27..c901e0f03e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -550,6 +550,8 @@ class EnumDef(AbstractAttrDef): passed items or list of values for multiselection. multiselection (Optional[bool]): If True, multiselection is allowed. Output is list of selected items. + placeholder (Optional[str]): Placeholder for UI purposes, only for + multiselection enumeration. """ type = "enum" From 5684c941deb0f2f62cf71f69667f786217ded612 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:51:17 +0100 Subject: [PATCH 482/546] 'CreatedInstance' allows to pass in transient data --- .../ayon_core/pipeline/create/structures.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index a45e053cca..17bb85b720 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -1,6 +1,7 @@ import copy import collections from uuid import uuid4 +import typing from typing import Optional, Dict, List, Any from ayon_core.lib.attribute_definitions import ( @@ -17,6 +18,9 @@ from ayon_core.pipeline import ( from .exceptions import ImmutableKeyError from .changes import TrackChangesItem +if typing.TYPE_CHECKING: + from .creator_plugins import BaseCreator + class ConvertorItem: """Item representing convertor plugin. @@ -444,10 +448,11 @@ class CreatedInstance: def __init__( self, - product_type, - product_name, - data, - creator, + product_type: str, + product_name: str, + data: Dict[str, Any], + creator: "BaseCreator", + transient_data: Optional[Dict[str, Any]] = None, ): self._creator = creator creator_identifier = creator.identifier @@ -462,7 +467,9 @@ class CreatedInstance: self._members = [] # Data that can be used for lifetime of object - self._transient_data = {} + if transient_data is None: + transient_data = {} + self._transient_data = transient_data # Create a copy of passed data to avoid changing them on the fly data = copy.deepcopy(data or {}) @@ -787,16 +794,26 @@ class CreatedInstance: self._create_context.instance_create_attr_defs_changed(self.id) @classmethod - def from_existing(cls, instance_data, creator): + def from_existing( + cls, + instance_data: Dict[str, Any], + creator: "BaseCreator", + transient_data: Optional[Dict[str, Any]] = None, + ) -> "CreatedInstance": """Convert instance data from workfile to CreatedInstance. Args: instance_data (Dict[str, Any]): Data in a structure ready for 'CreatedInstance' object. creator (BaseCreator): Creator plugin which is creating the - instance of for which the instance belong. - """ + instance of for which the instance belongs. + transient_data (Optional[dict[str, Any]]): Instance transient + data. + Returns: + CreatedInstance: Instance object. + + """ instance_data = copy.deepcopy(instance_data) product_type = instance_data.get("productType") @@ -809,7 +826,11 @@ class CreatedInstance: product_name = instance_data.get("subset") return cls( - product_type, product_name, instance_data, creator + product_type, + product_name, + instance_data, + creator, + transient_data=transient_data, ) def attribute_value_changed(self, key, changes): From f5bd7a9172f7a69aab66cecddb73ba973e03b50b Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Tue, 28 Jan 2025 15:22:06 +0100 Subject: [PATCH 483/546] Apply frame offset to timewarp to handle source frame offset. --- client/ayon_core/pipeline/editorial.py | 32 +++++++ .../test_media_range_with_retimes.py | 88 +++++++++---------- 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index e6e6294a81..8b6cfc52f1 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -441,6 +441,15 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): in_frame += time_scalar frame_range.append(in_frame) + # Different editorial DCC might have different TimeWarp logic. + # The following logic assumes that the "lookup" list values are + # frame offsets relative to the current source frame number. + # + # media_source_range |______1_____|______2______|______3______| + # + # media_retimed_range |______2_____|______2______|______3______| + # + # TimeWarp lookup +1 0 0 for tw_idx, tw in enumerate(time_warp_nodes): for idx, frame_number in enumerate(frame_range): # First timewarp, apply on media range @@ -467,9 +476,32 @@ def get_media_range_with_retimes(otio_clip, handle_start, handle_end): ) # adjust range if needed + media_in_trimmed_before_tw = media_in_trimmed media_in_trimmed = max(min(frame_range), media_in) media_out_trimmed = min(max(frame_range), media_out) + # If TimeWarp changes the first frame of the soure range, + # we need to offset the first TimeWarp values accordingly. + # + # expected_range |______2_____|______2______|______3______| + # + # EDITORIAL + # media_source_range |______1_____|______2______|______3______| + # + # TimeWarp lookup +1 0 0 + # + # EXTRACTED PLATE + # plate_range |______2_____|______3______|_ _ _ _ _ _ _| + # + # expected TimeWarp 0 -1 -1 + if media_in_trimmed != media_in_trimmed_before_tw: + offset = media_in_trimmed_before_tw - media_in_trimmed + offset *= 1.0 / time_scalar + time_warp_nodes[0]["lookup"] = [ + value + offset + for value in time_warp_nodes[0]["lookup"] + ] + # adjust available handles if needed if (media_in_trimmed - media_in) < handle_start: handle_start = max(0, media_in_trimmed - media_in) diff --git a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py index fbab60623f..112d00b3e4 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py +++ b/tests/client/ayon_core/pipeline/editorial/test_media_range_with_retimes.py @@ -231,17 +231,17 @@ def test_movie_timewarp(): 'Class': 'TimeWarp', 'length': 4.0, 'lookup': [ - 2.0, - 1.8959999809265136, - 1.767999971389771, - 1.59199997138977, - 1.3439999809265135, - 1.0, - 0.5440000181198119, - -0.007999974250793684, - -0.6319999756813051, - -1.3039999847412114, - -2.0 + 0.0, + -0.10400001907348644, + -0.23200002861022906, + -0.4080000286102301, + -0.6560000190734865, + -1.0, + -1.455999981880188, + -2.0079999742507937, + -2.631999975681305, + -3.3039999847412114, + -4.0 ], 'name': 'TimeWarp2' } @@ -581,17 +581,17 @@ def test_img_sequence_timewarp_beyond_range(): 'Class': 'TimeWarp', 'length': 1.0, 'lookup': [ - -5.0, - -3.9440000305175777, - -2.852000034332275, - -1.6880000228881844, - -0.4160000076293944, - 1.0, - 2.5839999923706056, - 4.311999977111817, - 6.147999965667726, - 8.055999969482421, - 10.0 + 0.0, + 1.0559999694824223, + 2.147999965667725, + 3.3119999771118156, + 4.583999992370606, + 6.0, + 7.583999992370606, + 9.311999977111817, + 11.147999965667726, + 13.055999969482421, + 15.0 ], 'name': 'TimeWarp3' } @@ -632,17 +632,17 @@ def test_img_sequence_2X_speed_timewarp(): 'Class': 'TimeWarp', 'length': 4.0, 'lookup': [ - 2.0, - 1.7039999923706055, - 1.431999991416931, - 1.2079999942779531, - 1.055999998092652, - 1.0, - 1.056000007629395, - 1.208000022888184, - 1.432000034332276, - 1.7040000305175766, - 2.0 + 0.0, + -0.2960000076293945, + -0.568000008583069, + -0.7920000057220469, + -0.944000001907348, + -1.0, + -0.9439999923706051, + -0.791999977111816, + -0.5679999656677239, + -0.29599996948242335, + 0.0 ], 'name': 'TimeWarp6' } @@ -682,17 +682,17 @@ def test_img_sequence_multiple_timewarps(): 'Class': 'TimeWarp', 'length': 1.0, 'lookup': [ - -5.0, - -3.9440000305175777, - -2.852000034332275, - -1.6880000228881844, - -0.4160000076293944, - 1.0, - 2.5839999923706056, - 4.311999977111817, - 6.147999965667726, - 8.055999969482421, - 10.0 + 0.0, + 1.0559999694824223, + 2.147999965667725, + 3.3119999771118156, + 4.583999992370606, + 6.0, + 7.583999992370606, + 9.311999977111817, + 11.147999965667726, + 13.055999969482421, + 15.0 ], 'name': 'TimeWarp3' }, From d25c4701d1ec09f1aa81b9a80b356e79e3215456 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Jan 2025 12:04:38 +0100 Subject: [PATCH 484/546] Fix Version follow up from Workfile set by hosts. --- .../ayon_core/pipeline/create/creator_plugins.py | 15 +++++++++++++-- .../plugins/publish/collect_scene_version.py | 7 +++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 445b41cb4b..f29d67f263 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -858,13 +858,24 @@ class Creator(BaseCreator): ["CollectAnatomyInstanceData"] ["follow_workfile_version"] ) + follow_version_hosts = ( + publish_settings + ["CollectSceneVersion"] + ["hosts"] + ) + + current_host = create_ctx.host.name + follow_workfile_version = ( + follow_workfile_version and + current_host in follow_version_hosts + ) # Gather version number provided from the instance. + current_workfile = create_ctx.get_current_workfile_path() version = instance.get("version") # If follow workfile, gather version from workfile path. - if version is None and follow_workfile_version: - current_workfile = self.create_context.get_current_workfile_path() + if version is None and follow_workfile_version and current_workfile: workfile_version = get_version_from_path(current_workfile) version = int(workfile_version) diff --git a/client/ayon_core/plugins/publish/collect_scene_version.py b/client/ayon_core/plugins/publish/collect_scene_version.py index 8d643062bc..8758bc2181 100644 --- a/client/ayon_core/plugins/publish/collect_scene_version.py +++ b/client/ayon_core/plugins/publish/collect_scene_version.py @@ -46,6 +46,13 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): self.log.debug("Skipping for headless publishing") return + if context.data["hostName"] not in self.hosts: + self.log.debug( + f"Host {context.data['hostName']} is" + " not setup for following version." + ) + return + if not context.data.get('currentFile'): self.log.error("Cannot get current workfile path. " "Make sure your scene is saved.") From 3731e1226a840996f68dd61579f8bb0a5ba38063 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Jan 2025 12:36:00 +0100 Subject: [PATCH 485/546] Fix lint + log message. --- client/ayon_core/pipeline/create/creator_plugins.py | 2 +- client/ayon_core/plugins/publish/collect_scene_version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index f29d67f263..a7f191bc44 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -866,7 +866,7 @@ class Creator(BaseCreator): current_host = create_ctx.host.name follow_workfile_version = ( - follow_workfile_version and + follow_workfile_version and current_host in follow_version_hosts ) diff --git a/client/ayon_core/plugins/publish/collect_scene_version.py b/client/ayon_core/plugins/publish/collect_scene_version.py index 8758bc2181..fcd57f4110 100644 --- a/client/ayon_core/plugins/publish/collect_scene_version.py +++ b/client/ayon_core/plugins/publish/collect_scene_version.py @@ -49,7 +49,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): if context.data["hostName"] not in self.hosts: self.log.debug( f"Host {context.data['hostName']} is" - " not setup for following version." + " not setup for collecting scene version." ) return From 1b109d761feb890a732b0995ead852c0a54db0e5 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Jan 2025 14:03:23 +0100 Subject: [PATCH 486/546] Fix host refresh in publish/lib.py --- client/ayon_core/pipeline/publish/lib.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 25495ed38b..62a68bc841 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -460,8 +460,19 @@ def filter_pyblish_plugins(plugins): ) apply_plugin_settings_automatically(plugin, plugin_settins, log) + # Pyblish already operated a filter based on host. + # But applying settings might have changed "hosts" + # value in plugin so re-filter. + plugin_hosts = getattr(plugin, "hosts", None) + if ( + plugin_hosts + and "*" not in plugin_hosts + and host_name not in plugin_hosts + ): + plugins.remove(plugin) + # Remove disabled plugins - if getattr(plugin, "enabled", True) is False: + elif getattr(plugin, "enabled", True) is False: plugins.remove(plugin) From 3f691607e54274370747c84abeb358a837f079aa Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Jan 2025 19:33:43 +0100 Subject: [PATCH 487/546] Fix [clip_media] reviewable. --- .../plugins/publish/collect_otio_review.py | 19 ++++++++++++++----- .../publish/collect_otio_subset_resources.py | 17 ----------------- .../plugins/publish/extract_otio_review.py | 3 +++ 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 4708b0a97c..36ef8f46d8 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -36,6 +36,16 @@ class CollectOtioReview(pyblish.api.InstancePlugin): # optionally get `reviewTrack` review_track_name = instance.data.get("reviewTrack") + # [clip_media] setting: + # Extract current clip source range as reviewable. + # Flag review content from otio_clip. + if not review_track_name and "review" in instance.data["families"]: + otio_review_clips = [otio_clip] + + # skip if no review track available + elif not review_track_name: + return + # generate range in parent otio_tl_range = otio_clip.range_in_parent() @@ -43,13 +53,12 @@ class CollectOtioReview(pyblish.api.InstancePlugin): clip_frame_end = int( otio_tl_range.start_time.value + otio_tl_range.duration.value) - # skip if no review track available - if not review_track_name: - return - # loop all tracks and match with name in `reviewTrack` for track in otio_timeline.tracks: - if review_track_name != track.name: + if ( + review_track_name is None + or review_track_name != track.name + ): continue # process correct track diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index deb51f62a5..d07c956856 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -195,13 +195,6 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, file=filename) - if ( - not instance.data.get("otioReviewClips") - and "review" in instance.data["families"] - ): - review_repre = self._create_representation( - frame_start, frame_end, collection=collection, - delete=True, review=True) else: _trim = False @@ -217,13 +210,6 @@ class CollectOtioSubsetResources( repre = self._create_representation( frame_start, frame_end, file=filename, trim=_trim) - if ( - not instance.data.get("otioReviewClips") - and "review" in instance.data["families"] - ): - review_repre = self._create_representation( - frame_start, frame_end, - file=filename, delete=True, review=True) instance.data["originalDirname"] = self.staging_dir @@ -236,9 +222,6 @@ class CollectOtioSubsetResources( instance.data["representations"].append(repre) - # add review representation to instance data - if review_repre: - instance.data["representations"].append(review_repre) self.log.debug(instance.data) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 275c3e7c58..c2788af77c 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -320,6 +320,9 @@ class ExtractOTIOReview( end = max(collection.indexes) files = [f for f in collection] + # single frame sequence + if len(files) == 1: + files = files[0] ext = collection.format("{tail}") representation_data.update({ "name": ext[1:], From fd63c97f4e9baf75027895c9460b548ed453ed8a Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Jan 2025 19:56:34 +0100 Subject: [PATCH 488/546] Address feedback from PR. --- client/ayon_core/pipeline/publish/lib.py | 7 +----- .../plugins/publish/collect_scene_version.py | 25 +------------------ 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 62a68bc841..9adfc5e9e2 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -463,12 +463,7 @@ def filter_pyblish_plugins(plugins): # Pyblish already operated a filter based on host. # But applying settings might have changed "hosts" # value in plugin so re-filter. - plugin_hosts = getattr(plugin, "hosts", None) - if ( - plugin_hosts - and "*" not in plugin_hosts - and host_name not in plugin_hosts - ): + if not pyblish.plugin.host_is_compatible(plugin): plugins.remove(plugin) # Remove disabled plugins diff --git a/client/ayon_core/plugins/publish/collect_scene_version.py b/client/ayon_core/plugins/publish/collect_scene_version.py index fcd57f4110..7979b66abe 100644 --- a/client/ayon_core/plugins/publish/collect_scene_version.py +++ b/client/ayon_core/plugins/publish/collect_scene_version.py @@ -14,23 +14,7 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder label = 'Collect Scene Version' # configurable in Settings - hosts = [ - "aftereffects", - "blender", - "celaction", - "fusion", - "harmony", - "hiero", - "houdini", - "maya", - "max", - "nuke", - "photoshop", - "resolve", - "tvpaint", - "motionbuilder", - "substancepainter" - ] + hosts = ["*"] # in some cases of headless publishing (for example webpublisher using PS) # you want to ignore version from name and let integrate use next version @@ -46,13 +30,6 @@ class CollectSceneVersion(pyblish.api.ContextPlugin): self.log.debug("Skipping for headless publishing") return - if context.data["hostName"] not in self.hosts: - self.log.debug( - f"Host {context.data['hostName']} is" - " not setup for collecting scene version." - ) - return - if not context.data.get('currentFile'): self.log.error("Cannot get current workfile path. " "Make sure your scene is saved.") From ca03c4d86d2c08b347f4363030ec32b0a96b7721 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Jan 2025 12:05:39 +0100 Subject: [PATCH 489/546] Add support for `optional_tooltip` attribute on `OptionalPyblishPluginMixin` --- client/ayon_core/pipeline/publish/publish_plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 57215eff68..94c0307ca0 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -304,8 +304,11 @@ class OptionalPyblishPluginMixin(AYONPyblishPluginMixin): active = getattr(cls, "active", True) # Return boolean stored under 'active' key with label of the class name label = cls.label or cls.__name__ + # Allow exposing tooltip from class with `optional_tooltip` attribute + tooltip = getattr(cls, "optional_tooltip", None) + return [ - BoolDef("active", default=active, label=label) + BoolDef("active", default=active, label=label, tooltip=tooltip) ] def is_active(self, data): From 64b6729eec537862bf05a515837936c5dff6b9a5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Jan 2025 12:40:53 +0100 Subject: [PATCH 490/546] Expose `optional_tooltip` directly as attribute on the `OptionalPyblishPluginMixin` for better auto-complete in IDEs --- client/ayon_core/pipeline/publish/publish_plugins.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 94c0307ca0..65a5a474ea 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -292,6 +292,9 @@ class OptionalPyblishPluginMixin(AYONPyblishPluginMixin): ``` """ + # Allow exposing tooltip from class with `optional_tooltip` attribute + optional_tooltip: Optional[str] = None + @classmethod def get_attribute_defs(cls): """Attribute definitions based on plugin's optional attribute.""" @@ -304,11 +307,12 @@ class OptionalPyblishPluginMixin(AYONPyblishPluginMixin): active = getattr(cls, "active", True) # Return boolean stored under 'active' key with label of the class name label = cls.label or cls.__name__ - # Allow exposing tooltip from class with `optional_tooltip` attribute - tooltip = getattr(cls, "optional_tooltip", None) return [ - BoolDef("active", default=active, label=label, tooltip=tooltip) + BoolDef("active", + default=active, + label=label, + tooltip=cls.optional_tooltip) ] def is_active(self, data): From e1438ed597550ee5b55e46efab3354838a42088a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 31 Jan 2025 23:30:04 +0100 Subject: [PATCH 491/546] Update client/ayon_core/pipeline/publish/publish_plugins.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/pipeline/publish/publish_plugins.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/publish/publish_plugins.py b/client/ayon_core/pipeline/publish/publish_plugins.py index 65a5a474ea..cc6887e762 100644 --- a/client/ayon_core/pipeline/publish/publish_plugins.py +++ b/client/ayon_core/pipeline/publish/publish_plugins.py @@ -309,10 +309,12 @@ class OptionalPyblishPluginMixin(AYONPyblishPluginMixin): label = cls.label or cls.__name__ return [ - BoolDef("active", - default=active, - label=label, - tooltip=cls.optional_tooltip) + BoolDef( + "active", + default=active, + label=label, + tooltip=cls.optional_tooltip, + ) ] def is_active(self, data): From 5c53d201244b1bf5d5914a23c936472b594fb6d9 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 3 Feb 2025 10:51:59 +0100 Subject: [PATCH 492/546] Address feedback from PR. --- client/ayon_core/pipeline/create/creator_plugins.py | 5 +++-- client/ayon_core/pipeline/publish/lib.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index a7f191bc44..69cd3894af 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -877,10 +877,11 @@ class Creator(BaseCreator): # If follow workfile, gather version from workfile path. if version is None and follow_workfile_version and current_workfile: workfile_version = get_version_from_path(current_workfile) - version = int(workfile_version) + if workfile_version is not None: + version = int(workfile_version) # Fill-up version with next version available. - elif version is None: + if version is None: versions = self.get_next_versions_for_instances( [instance] ) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 9adfc5e9e2..cc5f67c74b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -460,14 +460,14 @@ def filter_pyblish_plugins(plugins): ) apply_plugin_settings_automatically(plugin, plugin_settins, log) + # Remove disabled plugins + if getattr(plugin, "enabled", True) is False: + plugins.remove(plugin) + # Pyblish already operated a filter based on host. # But applying settings might have changed "hosts" # value in plugin so re-filter. - if not pyblish.plugin.host_is_compatible(plugin): - plugins.remove(plugin) - - # Remove disabled plugins - elif getattr(plugin, "enabled", True) is False: + elif not pyblish.plugin.host_is_compatible(plugin): plugins.remove(plugin) From 0293d74618c1a266215ca3051cb7d56da58f741c Mon Sep 17 00:00:00 2001 From: Robin De Lillo Date: Mon, 3 Feb 2025 11:25:34 +0100 Subject: [PATCH 493/546] Update client/ayon_core/plugins/publish/collect_otio_review.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/plugins/publish/collect_otio_review.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 36ef8f46d8..24f2f6c3e6 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -55,10 +55,11 @@ class CollectOtioReview(pyblish.api.InstancePlugin): # loop all tracks and match with name in `reviewTrack` for track in otio_timeline.tracks: - if ( - review_track_name is None - or review_track_name != track.name - ): + # Skip the loop + if review_track_name is None: + break + + if review_track_name != track.name: continue # process correct track From e12fa847c959cd4af1145810dcf74a38433232c2 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 3 Feb 2025 11:29:34 +0100 Subject: [PATCH 494/546] Address feedback from PR. --- client/ayon_core/plugins/publish/collect_otio_review.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_review.py b/client/ayon_core/plugins/publish/collect_otio_review.py index 24f2f6c3e6..064d4e3f3b 100644 --- a/client/ayon_core/plugins/publish/collect_otio_review.py +++ b/client/ayon_core/plugins/publish/collect_otio_review.py @@ -55,10 +55,12 @@ class CollectOtioReview(pyblish.api.InstancePlugin): # loop all tracks and match with name in `reviewTrack` for track in otio_timeline.tracks: - # Skip the loop + + # No review track defined, skip the loop if review_track_name is None: break + # Not current review track, skip it. if review_track_name != track.name: continue From 07ccade3a4062f10ae5a038a614b0f3260e406d6 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Mon, 3 Feb 2025 14:54:29 +0100 Subject: [PATCH 495/546] Fix lint. --- .../ayon_core/plugins/publish/collect_otio_subset_resources.py | 1 - client/ayon_core/plugins/publish/extract_otio_review.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index d07c956856..f1fa6a817d 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -158,7 +158,6 @@ class CollectOtioSubsetResources( self.log.info( "frame_start-frame_end: {}-{}".format(frame_start, frame_end)) - review_repre = None if is_sequence: # file sequence way diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index c2788af77c..2461195b27 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -322,7 +322,7 @@ class ExtractOTIOReview( files = [f for f in collection] # single frame sequence if len(files) == 1: - files = files[0] + files = files[0] ext = collection.format("{tail}") representation_data.update({ "name": ext[1:], From b048f0aa3bae1e6c3b963ab89b88b804e0208c2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:29:01 +0100 Subject: [PATCH 496/546] added helper method 'get_template_data' to create context --- client/ayon_core/pipeline/create/context.py | 31 +++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index e29971415d..36a05725a6 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -29,6 +29,7 @@ from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.attribute_definitions import get_default_values from ayon_core.host import IPublishHost, IWorkfileHost from ayon_core.pipeline import Anatomy +from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult from .exceptions import ( @@ -480,6 +481,36 @@ class CreateContext: self.get_current_project_name()) return self._current_project_settings + def get_template_data( + self, folder_path: Optional[str], task_name: Optional[str] + ) -> dict[str, Any]: + """Prepare template data for given context. + + Method is using cached entities and settings to prepare template data. + + Args: + folder_path (Optional[str]): Folder path. + task_name (Optional[str]): Task name. + + Returns: + dict[str, Any]: Template data. + + """ + project_entity = self.get_current_project_entity() + folder_entity = task_entity = None + if folder_path: + folder_entity = self.get_folder_entity(folder_path) + if task_name and folder_entity: + task_entity = self.get_task_entity(folder_path, task_name) + + return get_template_data( + project_entity, + folder_entity, + task_entity, + host_name=self.host_name, + settings=self.get_current_project_settings(), + ) + @property def context_has_changed(self): """Host context has changed. From cc1ec148656a7afbf0546d35cae93350742c886f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:46:52 +0100 Subject: [PATCH 497/546] fix index validation --- client/ayon_core/lib/path_templates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 057889403c..9e3e455a6c 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -587,8 +587,8 @@ class FormattingPart: if sub_key < 0: sub_key = len(value) + sub_key - invalid = 0 > sub_key < len(data) - if invalid: + valid = 0 <= sub_key < len(value) + if not valid: used_keys.append(sub_key) missing_key = True break From 43b18910bac326c00d0784b764641398738aca6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:51:08 +0100 Subject: [PATCH 498/546] fix typehint --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 36a05725a6..c169df67df 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -483,7 +483,7 @@ class CreateContext: def get_template_data( self, folder_path: Optional[str], task_name: Optional[str] - ) -> dict[str, Any]: + ) -> Dict[str, Any]: """Prepare template data for given context. Method is using cached entities and settings to prepare template data. From c21b95679f72099c93f02e3e55a7af3bafad402f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 4 Feb 2025 11:51:01 +0000 Subject: [PATCH 499/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 2775cb606a..da29c02004 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.0.15-dev" +__version__ = "1.1.0" diff --git a/package.py b/package.py index af3342f3f2..fe0c7dbd18 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.0.15-dev" +version = "1.1.0" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e040ce986f..32d101cc22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.0.15-dev" +version = "1.1.0" description = "" authors = ["Ynput Team "] readme = "README.md" From c7899fb3be31d215207c5e0cccf01a43e73408ff Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 4 Feb 2025 11:51:45 +0000 Subject: [PATCH 500/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index da29c02004..909ecd7a3c 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.1.0" +__version__ = "1.1.0+dev" diff --git a/package.py b/package.py index fe0c7dbd18..0b888f5c33 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.0" +version = "1.1.0+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 32d101cc22..32822391c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.1.0" +version = "1.1.0+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 516dd2d7cedfe1309aa033ce335d4c7130f3c693 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Feb 2025 15:07:05 +0100 Subject: [PATCH 501/546] Escape & on Windows in shell using ^& in `run_subprocess` --- client/ayon_core/lib/execute.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 95696fd272..35e931a5fc 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -122,6 +122,16 @@ def run_subprocess(*args, **kwargs): ) args = (new_arg, ) + # Escape & on Windows in shell using ^& + if ( + kwargs.get("shell") is True + and len(args) == 1 + and isinstance(args[0], str) + and platform.system().lower() == "windows" + ): + new_arg = args[0].replace("&", "^&") + args = (new_arg, ) + # Get environents from kwarg or use current process environments if were # not passed. env = kwargs.get("env") or os.environ From ed0f5c8d7f300a984c139ad7849779c4e781b456 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Feb 2025 15:20:28 +0100 Subject: [PATCH 502/546] Merge escape if checks + include `COMSPEC` check on Windows --- client/ayon_core/lib/execute.py | 36 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/lib/execute.py b/client/ayon_core/lib/execute.py index 35e931a5fc..516ea958f5 100644 --- a/client/ayon_core/lib/execute.py +++ b/client/ayon_core/lib/execute.py @@ -108,31 +108,29 @@ def run_subprocess(*args, **kwargs): | getattr(subprocess, "CREATE_NO_WINDOW", 0) ) - # Escape parentheses for bash + # Escape special characters in certain shells if ( kwargs.get("shell") is True and len(args) == 1 and isinstance(args[0], str) - and os.getenv("SHELL") in ("/bin/bash", "/bin/sh") ): - new_arg = ( - args[0] - .replace("(", "\\(") - .replace(")", "\\)") - ) - args = (new_arg, ) + # Escape parentheses for bash + if os.getenv("SHELL") in ("/bin/bash", "/bin/sh"): + new_arg = ( + args[0] + .replace("(", "\\(") + .replace(")", "\\)") + ) + args = (new_arg,) + # Escape & on Windows in shell with `cmd.exe` using ^& + elif ( + platform.system().lower() == "windows" + and os.getenv("COMSPEC").endswith("cmd.exe") + ): + new_arg = args[0].replace("&", "^&") + args = (new_arg, ) - # Escape & on Windows in shell using ^& - if ( - kwargs.get("shell") is True - and len(args) == 1 - and isinstance(args[0], str) - and platform.system().lower() == "windows" - ): - new_arg = args[0].replace("&", "^&") - args = (new_arg, ) - - # Get environents from kwarg or use current process environments if were + # Get environments from kwarg or use current process environments if were # not passed. env = kwargs.get("env") or os.environ # Make sure environment contains only strings From bfcfa264d97d2527905f2dc00df73c75b1c50a90 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Feb 2025 16:57:59 +0800 Subject: [PATCH 503/546] add substance designer as being part of the hosts in extract thumbnail --- client/ayon_core/plugins/publish/extract_thumbnail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 8ae18f4abf..bd2f7eb0ae 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -35,6 +35,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "resolve", "traypublisher", "substancepainter", + "substancedesigner", "nuke", "aftereffects", "unreal", From e13de28a24c2a223fe4903421f802a4ba7fcef08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:24:07 +0100 Subject: [PATCH 504/546] draw placeholder with disabled color --- client/ayon_core/tools/utils/multiselection_combobox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index b90838267b..e8bc688234 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -208,7 +208,8 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): draw_text = False if draw_text: option.currentText = combotext - option.palette.setCurrentColorGroup(QtGui.QPalette.Disabled) + # Draw text as disabled -> to mimic placeholder color + option.state &= ~QtWidgets.QStyle.State_Enabled painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) return From 71454b6fa4e31ce08028b08b813974c942f0ece6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:54:56 +0100 Subject: [PATCH 505/546] usa same placeholder color as elsewhere --- .../tools/utils/multiselection_combobox.py | 65 ++++++++++++------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index e8bc688234..7a8c4c2fcc 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -1,5 +1,7 @@ from qtpy import QtCore, QtGui, QtWidgets +from ayon_core.style import get_objected_colors + from .lib import ( checkstate_int_to_enum, checkstate_enum_to_int, @@ -49,11 +51,12 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): item_spacing = 5 item_bg_color = QtGui.QColor("#31424e") + _placeholder_color = None def __init__( self, parent=None, placeholder="", separator=", ", **kwargs ): - super(MultiSelectionComboBox, self).__init__(parent=parent, **kwargs) + super().__init__(parent=parent, **kwargs) self.setObjectName("MultiSelectionComboBox") self.setFocusPolicy(QtCore.Qt.StrongFocus) @@ -206,21 +209,23 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): combotext = self._placeholder_text else: draw_text = False - if draw_text: - option.currentText = combotext - # Draw text as disabled -> to mimic placeholder color - option.state &= ~QtWidgets.QStyle.State_Enabled - painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) - return - font_metricts = self.fontMetrics() + lines = self._lines + if draw_text: + color = self._get_placeholder_color() + pen = painter.pen() + pen.setColor(color) + painter.setPen(pen) + lines = {0: [combotext]} + + font_metrics = self.fontMetrics() if self._item_height is None: self.updateGeometry() self.update() return - for line, items in self._lines.items(): + for line, items in lines.items(): top_y = ( option.rect.top() + (line * self._item_height) @@ -228,7 +233,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): ) left_x = option.rect.left() + self.left_offset for item in items: - label_rect = font_metricts.boundingRect(item) + label_rect = font_metrics.boundingRect(item) label_height = label_rect.height() label_rect.moveTop(top_y) @@ -238,22 +243,23 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): label_rect.width() + self.left_right_padding ) - bg_rect = QtCore.QRectF(label_rect) - bg_rect.setWidth( - label_rect.width() + self.left_right_padding - ) - left_x = bg_rect.right() + self.item_spacing + if not draw_text: + bg_rect = QtCore.QRectF(label_rect) + bg_rect.setWidth( + label_rect.width() + self.left_right_padding + ) + left_x = bg_rect.right() + self.item_spacing + + bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) + bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) + + path = QtGui.QPainterPath() + path.addRoundedRect(bg_rect, 5, 5) + + painter.fillPath(path, self.item_bg_color) label_rect.moveLeft(label_rect.x() + self.left_right_padding) - bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) - bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) - - path = QtGui.QPainterPath() - path.addRoundedRect(bg_rect, 5, 5) - - painter.fillPath(path, self.item_bg_color) - painter.drawText( label_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, @@ -288,11 +294,11 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): line = 0 self._lines = {line: []} - font_metricts = self.fontMetrics() + font_metrics = self.fontMetrics() default_left_x = 0 + self.left_offset left_x = int(default_left_x) for item in items: - rect = font_metricts.boundingRect(item) + rect = font_metrics.boundingRect(item) width = rect.width() + (2 * self.left_right_padding) right_x = left_x + width if right_x > total_width: @@ -383,3 +389,12 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): return event.ignore() return super(MultiSelectionComboBox, self).keyPressEvent(event) + + @classmethod + def _get_placeholder_color(cls): + if cls._placeholder_color is None: + color_obj = get_objected_colors("font") + color = color_obj.get_qcolor() + color.setAlpha(67) + cls._placeholder_color = color + return cls._placeholder_color From e30b89e36d23a97a01f7fe22086c5b4f8d1291d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Feb 2025 12:09:12 +0100 Subject: [PATCH 506/546] fix formatting --- client/ayon_core/tools/utils/multiselection_combobox.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index 7a8c4c2fcc..7bd7a76abc 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -47,7 +47,7 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): top_bottom_padding = 2 left_right_padding = 3 left_offset = 4 - top_bottom_margins = 2 + top_bottom_margins = 1 item_spacing = 5 item_bg_color = QtGui.QColor("#31424e") @@ -250,7 +250,9 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): ) left_x = bg_rect.right() + self.item_spacing - bg_rect.setHeight(label_height + (2 * self.top_bottom_padding)) + bg_rect.setHeight( + label_height + (2 * self.top_bottom_padding) + ) bg_rect.moveTop(bg_rect.top() + self.top_bottom_margins) path = QtGui.QPainterPath() From 8a76ca9af74b7bb013017cd70f4a28eeb96927af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Feb 2025 14:20:56 +0100 Subject: [PATCH 507/546] Hide AutoCreator and HiddenCreator Creators from the 'CreatePlaceholder' lists --- .../pipeline/workfile/workfile_template_builder.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 4412e4489b..f607f18431 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -54,6 +54,7 @@ from ayon_core.pipeline.plugin_discover import ( from ayon_core.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, + HiddenCreator ) _NOT_SET = object() @@ -309,7 +310,12 @@ class AbstractTemplateBuilder(ABC): self._creators_by_name = creators_by_name def _collect_creators(self): - self._creators_by_name = dict(self.create_context.creators) + self._creators_by_name = { + name: creator for name, creator + in self.create_context.manual_creators.items() + # Do not list HiddenCreator even though it is a 'manual creator' + if not isinstance(creator, HiddenCreator) + } def get_creators_by_name(self): if self._creators_by_name is None: From d97cff327ac61ef329f3446fc1472f6cf63f0378 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Feb 2025 14:21:15 +0100 Subject: [PATCH 508/546] Cosmetics --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index f607f18431..af3694e6e6 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -54,7 +54,7 @@ from ayon_core.pipeline.plugin_discover import ( from ayon_core.pipeline.create import ( discover_legacy_creator_plugins, CreateContext, - HiddenCreator + HiddenCreator, ) _NOT_SET = object() From c9e2b05636cd87ec094bd478324535fd324fad81 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 6 Feb 2025 14:24:17 +0100 Subject: [PATCH 509/546] Update client/ayon_core/pipeline/workfile/workfile_template_builder.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index af3694e6e6..27da278c5e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -311,7 +311,8 @@ class AbstractTemplateBuilder(ABC): def _collect_creators(self): self._creators_by_name = { - name: creator for name, creator + identifier: creator + for identifier, creator in self.create_context.manual_creators.items() # Do not list HiddenCreator even though it is a 'manual creator' if not isinstance(creator, HiddenCreator) From ece0631c680891800a98afd391b1b9f129af3721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 6 Feb 2025 14:29:05 +0100 Subject: [PATCH 510/546] :dog: update ruff action --- .github/workflows/pr_linting.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr_linting.yml b/.github/workflows/pr_linting.yml index 3d2431b69a..896d5b7f4d 100644 --- a/.github/workflows/pr_linting.yml +++ b/.github/workflows/pr_linting.yml @@ -21,4 +21,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 + - uses: astral-sh/ruff-action@v1 + with: + changed-files: "true" From e0507d99d2829fd6296878899468146f92b5de0d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Feb 2025 22:20:27 +0800 Subject: [PATCH 511/546] add substance designer into the template --- server/settings/tools.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 96851be1da..0aaa88a8b0 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -484,6 +484,17 @@ DEFAULT_TOOLS_VALUES = { "task_types": [], "tasks": [], "template": "{folder[name]}_{variant}" + }, + { + "product_types": [ + "textureSet" + ], + "hosts": [ + "substancedesigner" + ], + "task_types": [], + "tasks": [], + "template": "T_{folder[name]}{variant}" } ], "filter_creator_profiles": [] @@ -548,10 +559,13 @@ DEFAULT_TOOLS_VALUES = { }, { "product_types": [ - "simpleUnrealTexture" + "simpleUnrealTexture", + "image", + "textures" ], "hosts": [ - "standalonepublisher" + "standalonepublisher", + "substancedesigner" ], "task_types": [], "task_names": [], @@ -595,10 +609,13 @@ DEFAULT_TOOLS_VALUES = { "hero_template_name_profiles": [ { "product_types": [ - "simpleUnrealTexture" + "simpleUnrealTexture", + "image", + "textures" ], "hosts": [ - "standalonepublisher" + "standalonepublisher", + "substancedesigner" ], "task_types": [], "task_names": [], From c6f5988aae75a1fb514f20ec56bb50985b145bea Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:13:29 +0100 Subject: [PATCH 512/546] use same font size for placeholder as elsewhere --- .../tools/utils/multiselection_combobox.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/utils/multiselection_combobox.py b/client/ayon_core/tools/utils/multiselection_combobox.py index 7bd7a76abc..a6198abb51 100644 --- a/client/ayon_core/tools/utils/multiselection_combobox.py +++ b/client/ayon_core/tools/utils/multiselection_combobox.py @@ -210,22 +210,36 @@ class MultiSelectionComboBox(QtWidgets.QComboBox): else: draw_text = False - lines = self._lines if draw_text: color = self._get_placeholder_color() pen = painter.pen() pen.setColor(color) painter.setPen(pen) - lines = {0: [combotext]} - font_metrics = self.fontMetrics() + left_x = option.rect.left() + self.left_offset + + font = self.font() + # This is hardcoded point size from styles + font.setPointSize(10) + painter.setFont(font) + + label_rect = QtCore.QRect(option.rect) + label_rect.moveLeft(left_x) + + painter.drawText( + label_rect, + QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, + combotext + ) + return if self._item_height is None: self.updateGeometry() self.update() return - for line, items in lines.items(): + font_metrics = self.fontMetrics() + for line, items in self._lines.items(): top_y = ( option.rect.top() + (line * self._item_height) From a409d55b5904525797f0520c4a872a7776ad1123 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Feb 2025 16:53:57 +0800 Subject: [PATCH 513/546] big roy's comment - separate template for substancedesigner from standalonepublisher --- server/settings/tools.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/server/settings/tools.py b/server/settings/tools.py index 0aaa88a8b0..32c72e7a98 100644 --- a/server/settings/tools.py +++ b/server/settings/tools.py @@ -559,12 +559,21 @@ DEFAULT_TOOLS_VALUES = { }, { "product_types": [ - "simpleUnrealTexture", - "image", - "textures" + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTexture" + }, + { + "product_types": [ + "image", + "textures", ], "hosts": [ - "standalonepublisher", "substancedesigner" ], "task_types": [], @@ -609,12 +618,21 @@ DEFAULT_TOOLS_VALUES = { "hero_template_name_profiles": [ { "product_types": [ - "simpleUnrealTexture", + "simpleUnrealTexture" + ], + "hosts": [ + "standalonepublisher" + ], + "task_types": [], + "task_names": [], + "template_name": "simpleUnrealTextureHero" + }, + { + "product_types": [ "image", "textures" ], "hosts": [ - "standalonepublisher", "substancedesigner" ], "task_types": [], From cdf7af65bf9ecad7fe6bb1fee7760ab22bf0cc6b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:20:56 +0100 Subject: [PATCH 514/546] pass 'AYON_USE_STAGING' instead of 'AYON_DEFAULT_SETTINGS_VARIANT' --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index ee88985905..2a58b580cd 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -32,7 +32,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): for key in [ "AYON_BUNDLE_NAME", - "AYON_DEFAULT_SETTINGS_VARIANT", + "AYON_USE_STAGING", "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", From ccd5e447842b632ffad3f0ec352db8d6ccedf799 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Feb 2025 11:52:03 +0100 Subject: [PATCH 515/546] keep filling 'AYON_DEFAULT_SETTINGS_VARIANT' for now --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 2a58b580cd..7ee3356cee 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -36,6 +36,8 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", + # DEPRECATED remove when deadline stops using it (added in 1.1.1) + "AYON_DEFAULT_SETTINGS_VARIANT", ]: value = os.getenv(key) if value: From 4b51f6479f391e853642a24059193fa3c244a772 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 7 Feb 2025 12:16:03 +0100 Subject: [PATCH 516/546] change added version --- client/ayon_core/plugins/publish/collect_farm_env_variables.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_farm_env_variables.py b/client/ayon_core/plugins/publish/collect_farm_env_variables.py index 7ee3356cee..2782ea86ac 100644 --- a/client/ayon_core/plugins/publish/collect_farm_env_variables.py +++ b/client/ayon_core/plugins/publish/collect_farm_env_variables.py @@ -36,7 +36,7 @@ class CollectCoreJobEnvVars(pyblish.api.ContextPlugin): "AYON_IN_TESTS", # NOTE Not sure why workdir is needed? "AYON_WORKDIR", - # DEPRECATED remove when deadline stops using it (added in 1.1.1) + # DEPRECATED remove when deadline stops using it (added in 1.1.2) "AYON_DEFAULT_SETTINGS_VARIANT", ]: value = os.getenv(key) From a711f346fbf19de3d59b930ecc0d4f9a2b196f8e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Feb 2025 17:49:07 +0100 Subject: [PATCH 517/546] don't use 'six' --- .../tools/pyblish_pype/vendor/qtawesome/iconic_font.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py b/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py index c25739aff8..ce95f9e74f 100644 --- a/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py +++ b/client/ayon_core/tools/pyblish_pype/vendor/qtawesome/iconic_font.py @@ -5,7 +5,6 @@ from __future__ import print_function import json import os -import six from qtpy import QtCore, QtGui @@ -152,7 +151,7 @@ class IconicFont(QtCore.QObject): def hook(obj): result = {} for key in obj: - result[key] = six.unichr(int(obj[key], 16)) + result[key] = chr(int(obj[key], 16)) return result if directory is None: From ac54c441ecf2d28892e3bd115598bbda459dace8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 10 Feb 2025 19:01:24 +0100 Subject: [PATCH 518/546] remove six from pyblish pype --- client/ayon_core/tools/pyblish_pype/model.py | 5 ++--- client/ayon_core/tools/pyblish_pype/util.py | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/pyblish_pype/model.py b/client/ayon_core/tools/pyblish_pype/model.py index 3a402f386e..44f951fe14 100644 --- a/client/ayon_core/tools/pyblish_pype/model.py +++ b/client/ayon_core/tools/pyblish_pype/model.py @@ -31,7 +31,6 @@ from . import settings, util from .awesome import tags as awesome from qtpy import QtCore, QtGui import qtawesome -from six import text_type from .constants import PluginStates, InstanceStates, GroupStates, Roles @@ -985,7 +984,7 @@ class TerminalModel(QtGui.QStandardItemModel): record_item = record else: record_item = { - "label": text_type(record.msg), + "label": str(record.msg), "type": "record", "levelno": record.levelno, "threadName": record.threadName, @@ -993,7 +992,7 @@ class TerminalModel(QtGui.QStandardItemModel): "filename": record.filename, "pathname": record.pathname, "lineno": record.lineno, - "msg": text_type(record.msg), + "msg": str(record.msg), "msecs": record.msecs, "levelname": record.levelname } diff --git a/client/ayon_core/tools/pyblish_pype/util.py b/client/ayon_core/tools/pyblish_pype/util.py index d24b07a409..081f7775d5 100644 --- a/client/ayon_core/tools/pyblish_pype/util.py +++ b/client/ayon_core/tools/pyblish_pype/util.py @@ -10,7 +10,6 @@ import sys import collections from qtpy import QtCore -from six import text_type import pyblish.api root = os.path.dirname(__file__) @@ -64,7 +63,7 @@ def u_print(msg, **kwargs): **kwargs: Keyword argument for `print` function. """ - if isinstance(msg, text_type): + if isinstance(msg, str): encoding = None try: encoding = os.getenv('PYTHONIOENCODING', sys.stdout.encoding) From 18c1ef04e60b433cf1e1fb9d89d87f6418d4f689 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:44:03 +0100 Subject: [PATCH 519/546] use platformdirs instead of appdirs --- client/ayon_core/lib/local_settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 08030ae87e..eff0068f00 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -9,7 +9,7 @@ from datetime import datetime from abc import ABC, abstractmethod from functools import lru_cache -import appdirs +import platformdirs import ayon_api _PLACEHOLDER = object() @@ -17,7 +17,7 @@ _PLACEHOLDER = object() def _get_ayon_appdirs(*args): return os.path.join( - appdirs.user_data_dir("AYON", "Ynput"), + platformdirs.user_data_dir("AYON", "Ynput"), *args ) From 1d8d417e53856f1be8d1f88b066710468504ec50 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:53:51 +0100 Subject: [PATCH 520/546] added acre functionality to lib functions --- client/ayon_core/lib/env_tools.py | 255 +++++++++++++++++++++++++++++- 1 file changed, 252 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/lib/env_tools.py b/client/ayon_core/lib/env_tools.py index 25bcbf7c1b..6ed67d7270 100644 --- a/client/ayon_core/lib/env_tools.py +++ b/client/ayon_core/lib/env_tools.py @@ -1,7 +1,39 @@ +from __future__ import annotations import os +import re +import platform +import typing +import collections +from string import Formatter +from typing import Optional + +if typing.TYPE_CHECKING: + from typing import Union, Literal + + PlatformName = Literal["windows", "linux", "darwin"] + EnvValue = Union[str, list[str], dict[str, str], dict[str, list[str]]] + +Results = collections.namedtuple( + "Results", + ["sorted", "cyclic"] +) -def env_value_to_bool(env_key=None, value=None, default=False): +class CycleError(ValueError): + """Raised when a cycle is detected in dynamic env variables compute.""" + pass + + +class DynamicKeyClashError(Exception): + """Raised when dynamic key clashes with an existing key.""" + pass + + +def env_value_to_bool( + env_key: Optional[str] = None, + value: Optional[str] = None, + default: bool = False, +) -> bool: """Convert environment variable value to boolean. Function is based on value of the environemt variable. Value is lowered @@ -11,6 +43,7 @@ def env_value_to_bool(env_key=None, value=None, default=False): bool: If value match to one of ["true", "yes", "1"] result if True but if value match to ["false", "no", "0"] result is False else default value is returned. + """ if value is None and env_key is None: return default @@ -27,7 +60,11 @@ def env_value_to_bool(env_key=None, value=None, default=False): return default -def get_paths_from_environ(env_key=None, env_value=None, return_first=False): +def get_paths_from_environ( + env_key: Optional[str] = None, + env_value: Optional[str] = None, + return_first: bool = False, +) -> Optional[Union[str, list[str]]]: """Return existing paths from specific environment variable. Args: @@ -38,7 +75,8 @@ def get_paths_from_environ(env_key=None, env_value=None, return_first=False): paths. `None` or empty list returned if nothing found. Returns: - str, list, None: Result of found path/s. + Optional[Union[str, list[str]]]: Result of found path/s. + """ existing_paths = [] if not env_key and not env_value: @@ -69,3 +107,214 @@ def get_paths_from_environ(env_key=None, env_value=None, return_first=False): return None # Return all existing paths from environment variable return existing_paths + + +def parse_env_variables_structure( + env: dict[str, EnvValue], + platform_name: Optional[PlatformName] = None +) -> dict[str, str]: + """Parse environment for platform-specific values and paths as lists. + + Args: + env (dict): The source environment to read. + platform_name (Optional[PlatformName]): Name of platform to parse for. + Defaults to current platform. + + Returns: + dict: The flattened environment for a platform. + + """ + platform_name = platform_name or platform.system().lower() + + result = {} + for variable, value in env.items(): + # Platform specific values + if isinstance(value, dict): + value = value.get(platform_name) + + # Allow to have lists as values in the tool data + if isinstance(value, (list, tuple)): + value = os.pathsep.join(value) + + if not value: + continue + + if not isinstance(value, str): + raise TypeError(f"Expected 'str' got '{type(value)}'") + + result[variable] = value + + return result + + +def _topological_sort(dependency_pairs): + """Sort values subject to dependency constraints""" + num_heads = collections.defaultdict(int) # num arrows pointing in + tails = collections.defaultdict(list) # list of arrows going out + heads = [] # unique list of heads in order first seen + for h, t in dependency_pairs: + num_heads[t] += 1 + if h in tails: + tails[h].append(t) + else: + tails[h] = [t] + heads.append(h) + + ordered = [h for h in heads if h not in num_heads] + for h in ordered: + for t in tails[h]: + num_heads[t] -= 1 + if not num_heads[t]: + ordered.append(t) + cyclic = [n for n, heads in num_heads.items() if heads] + return Results(ordered, cyclic) + + +def _partial_format( + s: str, + data: dict[str, str], + missing: Optional[str] = None, +) -> str: + """Return string `s` formatted by `data` allowing a partial format + + Arguments: + s (str): The string that will be formatted + data (dict): The dictionary used to format with. + + Example: + >>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"}) + 'left {a} and {c} left' + """ + + if missing is None: + missing = "{{{key}}}" + + class FormatDict(dict): + """This supports partial formatting. + + Missing keys are replaced with the return value of __missing__. + + """ + + def __missing__(self, key): + return missing.format(key=key) + + formatter = Formatter() + mapping = FormatDict(**data) + try: + f = formatter.vformat(s, (), mapping) + except Exception: + r_token = re.compile(r"({.*?})") + matches = re.findall(r_token, s) + f = s + for m in matches: + try: + f = re.sub(m, m.format(**data), f) + except (KeyError, ValueError): + continue + return f + + +def compute_env_variables_structure( + env: dict[str, str], + fill_dynamic_keys: bool = True, +) -> dict[str, str]: + """Compute the result from recursive dynamic environment. + + Note: Keys that are not present in the data will remain unformatted as the + original keys. So they can be formatted against the current user + environment when merging. So {"A": "{key}"} will remain {key} if not + present in the dynamic environment. + + """ + env = env.copy() + + # Collect dependencies + dependencies = [] + for key, value in env.items(): + try: + dependent_keys = re.findall("{(.+?)}", value) + for dependency in dependent_keys: + # Ignore direct references to itself because + # we don't format with itself anyway + if dependency == key: + continue + + dependencies.append((key, dependency)) + except Exception: + dependencies.append((key, value)) + + result = _topological_sort(dependencies) + + # Check cycle + if result.cyclic: + raise CycleError(f"A cycle is detected on: {result.cyclic}") + + # Format dynamic values + for key in reversed(result.sorted): + if key in env: + if not isinstance(env[key], str): + continue + data = env.copy() + data.pop(key) # format without itself + env[key] = _partial_format(env[key], data=data) + + # Format cyclic values + for key in result.cyclic: + if key in env: + if not isinstance(env[key], str): + continue + data = env.copy() + data.pop(key) # format without itself + env[key] = _partial_format(env[key], data=data) + + # Format dynamic keys + if fill_dynamic_keys: + formatted = {} + for key, value in env.items(): + if not isinstance(value, str): + formatted[key] = value + continue + + new_key = _partial_format(key, data=env) + if new_key in formatted: + raise DynamicKeyClashError( + f"Key clashes on: {new_key} (source: {key})" + ) + + formatted[new_key] = value + env = formatted + + return env + + +def merge_env_variables( + src_env: dict[str, str], + dst_env: dict[str, str], + missing: Optional[str] = None, +): + """Merge the tools environment with the 'current_env'. + + This finalizes the join with a current environment by formatting the + remainder of dynamic variables with that from the current environment. + + Remaining missing variables result in an empty value. + + Args: + src_env (dict): The dynamic environment + dst_env (dict): The target environment variables mapping to merge + the dynamic environment into. + missing (str): Argument passed to '_partial_format' during merging. + `None` should keep missing keys unchanged. + + Returns: + dict: The resulting environment after the merge. + + """ + result = dst_env.copy() + for key, value in src_env.items(): + result[key] = _partial_format( + str(value), data=dst_env, missing=missing + ) + + return result From b0927595a26a39f7b7c4f8c91a3fec3ca6bb590e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:54:24 +0100 Subject: [PATCH 521/546] use new functions in cli.py --- client/ayon_core/cli.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 6b4a1f824f..1287534bbf 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -8,7 +8,6 @@ from pathlib import Path import warnings import click -import acre from ayon_core import AYON_CORE_ROOT from ayon_core.addon import AddonsManager @@ -18,6 +17,11 @@ from ayon_core.lib import ( is_running_from_build, Logger, ) +from ayon_core.lib.env_tools import ( + parse_env_variables_structure, + compute_env_variables_structure, + merge_env_variables, +) @@ -240,14 +244,13 @@ def _set_global_environments() -> None: # first resolve general environment because merge doesn't expect # values to be list. # TODO: switch to AYON environment functions - merged_env = acre.merge( - acre.compute(acre.parse(general_env), cleanup=False), + merged_env = merge_env_variables( + compute_env_variables_structure( + parse_env_variables_structure(general_env) + ), dict(os.environ) ) - env = acre.compute( - merged_env, - cleanup=False - ) + env = compute_env_variables_structure(merged_env) os.environ.clear() os.environ.update(env) @@ -263,8 +266,8 @@ def _set_addons_environments(addons_manager): # Merge environments with current environments and update values if module_envs := addons_manager.collect_global_environments(): - parsed_envs = acre.parse(module_envs) - env = acre.merge(parsed_envs, dict(os.environ)) + parsed_envs = parse_env_variables_structure(module_envs) + env = merge_env_variables(parsed_envs, dict(os.environ)) os.environ.clear() os.environ.update(env) From 74443c92e71c3e79cbe7dcc61528703b0ed8faad Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:37:00 +0100 Subject: [PATCH 522/546] small tweaks --- client/ayon_core/cli.py | 11 +-- client/ayon_core/lib/env_tools.py | 151 +++++++++++++++--------------- 2 files changed, 81 insertions(+), 81 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 1287534bbf..d7cd3ba7f5 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -239,15 +239,12 @@ def version(build): def _set_global_environments() -> None: """Set global AYON environments.""" - general_env = get_general_environments() + # First resolve general environment + general_env = parse_env_variables_structure(get_general_environments()) - # first resolve general environment because merge doesn't expect - # values to be list. - # TODO: switch to AYON environment functions + # Merge environments with current environments and update values merged_env = merge_env_variables( - compute_env_variables_structure( - parse_env_variables_structure(general_env) - ), + compute_env_variables_structure(general_env), dict(os.environ) ) env = compute_env_variables_structure(merged_env) diff --git a/client/ayon_core/lib/env_tools.py b/client/ayon_core/lib/env_tools.py index 6ed67d7270..c71350869e 100644 --- a/client/ayon_core/lib/env_tools.py +++ b/client/ayon_core/lib/env_tools.py @@ -13,11 +13,6 @@ if typing.TYPE_CHECKING: PlatformName = Literal["windows", "linux", "darwin"] EnvValue = Union[str, list[str], dict[str, str], dict[str, list[str]]] -Results = collections.namedtuple( - "Results", - ["sorted", "cyclic"] -) - class CycleError(ValueError): """Raised when a cycle is detected in dynamic env variables compute.""" @@ -124,7 +119,8 @@ def parse_env_variables_structure( dict: The flattened environment for a platform. """ - platform_name = platform_name or platform.system().lower() + if platform_name is None: + platform_name = platform.system().lower() result = {} for variable, value in env.items(): @@ -147,72 +143,94 @@ def parse_env_variables_structure( return result -def _topological_sort(dependency_pairs): - """Sort values subject to dependency constraints""" +def _topological_sort( + dependencies: dict[str, set[str]] +) -> tuple[list[str], list[str]]: + """Sort values subject to dependency constraints. + + Args: + dependencies (dict[str, set[str]): Mapping of environment variable + keys to a set of keys they depend on. + + Returns: + tuple[list[str], list[str]]: A tuple of two lists. The first list + contains the ordered keys in which order should be environment + keys filled, the second list contains the keys that would cause + cyclic fill of values. + + """ num_heads = collections.defaultdict(int) # num arrows pointing in tails = collections.defaultdict(list) # list of arrows going out heads = [] # unique list of heads in order first seen - for h, t in dependency_pairs: - num_heads[t] += 1 - if h in tails: - tails[h].append(t) - else: - tails[h] = [t] - heads.append(h) + for head, tail_values in dependencies.items(): + for tail_value in tail_values: + num_heads[tail_value] += 1 + if head not in tails: + heads.append(head) + tails[head].append(tail_value) - ordered = [h for h in heads if h not in num_heads] - for h in ordered: - for t in tails[h]: - num_heads[t] -= 1 - if not num_heads[t]: - ordered.append(t) - cyclic = [n for n, heads in num_heads.items() if heads] - return Results(ordered, cyclic) + ordered = [head for head in heads if head not in num_heads] + for head in ordered: + for tail in tails[head]: + num_heads[tail] -= 1 + if not num_heads[tail]: + ordered.append(tail) + cyclic = [tail for tail, heads in num_heads.items() if heads] + return ordered, cyclic + + +class _PartialFormatDict(dict): + """This supports partial formatting. + + Missing keys are replaced with the return value of __missing__. + + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._missing_template: str = "{{{key}}}" + + def set_missing_template(self, template: str): + self._missing_template = template + + def __missing__(self, key: str) -> str: + return self._missing_template.format(key=key) def _partial_format( - s: str, + value: str, data: dict[str, str], - missing: Optional[str] = None, + missing_template: Optional[str] = None, ) -> str: """Return string `s` formatted by `data` allowing a partial format Arguments: - s (str): The string that will be formatted + value (str): The string that will be formatted data (dict): The dictionary used to format with. + missing_template (Optional[str]): The template to use when a key is + missing from the data. If `None`, the key will remain unformatted. Example: >>> _partial_format("{d} {a} {b} {c} {d}", {'b': "and", 'd': "left"}) 'left {a} and {c} left' + """ - if missing is None: - missing = "{{{key}}}" - - class FormatDict(dict): - """This supports partial formatting. - - Missing keys are replaced with the return value of __missing__. - - """ - - def __missing__(self, key): - return missing.format(key=key) + mapping = _PartialFormatDict(**data) + if missing_template is not None: + mapping.set_missing_template(missing_template) formatter = Formatter() - mapping = FormatDict(**data) try: - f = formatter.vformat(s, (), mapping) + output = formatter.vformat(value, (), mapping) except Exception: r_token = re.compile(r"({.*?})") - matches = re.findall(r_token, s) - f = s - for m in matches: + output = value + for match in re.findall(r_token, value): try: - f = re.sub(m, m.format(**data), f) - except (KeyError, ValueError): + output = re.sub(match, match.format(**data), output) + except (KeyError, ValueError, IndexError): continue - return f + return output def compute_env_variables_structure( @@ -230,28 +248,22 @@ def compute_env_variables_structure( env = env.copy() # Collect dependencies - dependencies = [] + dependencies = collections.defaultdict(set) for key, value in env.items(): - try: - dependent_keys = re.findall("{(.+?)}", value) - for dependency in dependent_keys: - # Ignore direct references to itself because - # we don't format with itself anyway - if dependency == key: - continue + dependent_keys = re.findall("{(.+?)}", value) + for dependent_key in dependent_keys: + # Ignore reference to itself or key is not in env + if dependent_key != key and dependent_key in env: + dependencies[key].add(dependent_key) - dependencies.append((key, dependency)) - except Exception: - dependencies.append((key, value)) - - result = _topological_sort(dependencies) + ordered, cyclic = _topological_sort(dependencies) # Check cycle - if result.cyclic: - raise CycleError(f"A cycle is detected on: {result.cyclic}") + if cyclic: + raise CycleError(f"A cycle is detected on: {cyclic}") # Format dynamic values - for key in reversed(result.sorted): + for key in reversed(ordered): if key in env: if not isinstance(env[key], str): continue @@ -259,15 +271,6 @@ def compute_env_variables_structure( data.pop(key) # format without itself env[key] = _partial_format(env[key], data=data) - # Format cyclic values - for key in result.cyclic: - if key in env: - if not isinstance(env[key], str): - continue - data = env.copy() - data.pop(key) # format without itself - env[key] = _partial_format(env[key], data=data) - # Format dynamic keys if fill_dynamic_keys: formatted = {} @@ -291,7 +294,7 @@ def compute_env_variables_structure( def merge_env_variables( src_env: dict[str, str], dst_env: dict[str, str], - missing: Optional[str] = None, + missing_template: Optional[str] = None, ): """Merge the tools environment with the 'current_env'. @@ -304,7 +307,7 @@ def merge_env_variables( src_env (dict): The dynamic environment dst_env (dict): The target environment variables mapping to merge the dynamic environment into. - missing (str): Argument passed to '_partial_format' during merging. + missing_template (str): Argument passed to '_partial_format' during merging. `None` should keep missing keys unchanged. Returns: @@ -314,7 +317,7 @@ def merge_env_variables( result = dst_env.copy() for key, value in src_env.items(): result[key] = _partial_format( - str(value), data=dst_env, missing=missing + str(value), dst_env, missing_template ) return result From 1d7036ffed2ddbb2610212a999ce8492f2d15386 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 15:49:48 +0100 Subject: [PATCH 523/546] formatting fixes --- client/ayon_core/lib/env_tools.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/env_tools.py b/client/ayon_core/lib/env_tools.py index c71350869e..c1bfe0c292 100644 --- a/client/ayon_core/lib/env_tools.py +++ b/client/ayon_core/lib/env_tools.py @@ -295,7 +295,7 @@ def merge_env_variables( src_env: dict[str, str], dst_env: dict[str, str], missing_template: Optional[str] = None, -): +) -> dict[str, str]: """Merge the tools environment with the 'current_env'. This finalizes the join with a current environment by formatting the @@ -307,11 +307,11 @@ def merge_env_variables( src_env (dict): The dynamic environment dst_env (dict): The target environment variables mapping to merge the dynamic environment into. - missing_template (str): Argument passed to '_partial_format' during merging. - `None` should keep missing keys unchanged. + missing_template (str): Argument passed to '_partial_format' during + merging. `None` should keep missing keys unchanged. Returns: - dict: The resulting environment after the merge. + dict[str, str]: The resulting environment after the merge. """ result = dst_env.copy() From 6a57b4c4504bb19987a3e10bcf8f167e4ae121b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:13:16 +0100 Subject: [PATCH 524/546] use odd numbers for frame size --- client/ayon_core/tools/utils/nice_checkbox.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/utils/nice_checkbox.py b/client/ayon_core/tools/utils/nice_checkbox.py index 06845c397a..b47e3e62e6 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -328,6 +328,9 @@ class NiceCheckbox(QtWidgets.QFrame): if frame_rect.width() < 0 or frame_rect.height() < 0: return + frame_rect.setLeft(frame_rect.x() + (frame_rect.width() % 2)) + frame_rect.setTop(frame_rect.y() + (frame_rect.height() % 2)) + painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing) @@ -364,11 +367,16 @@ class NiceCheckbox(QtWidgets.QFrame): margin_size_c = 0 checkbox_rect = QtCore.QRect( - frame_rect.x() + margin_size_c, - frame_rect.y() + margin_size_c, - frame_rect.width() - (margin_size_c * 2), - frame_rect.height() - (margin_size_c * 2) + frame_rect.x(), + frame_rect.y(), + frame_rect.width(), + frame_rect.height() ) + if margin_size_c: + checkbox_rect.adjust( + margin_size_c, margin_size_c, + -margin_size_c, -margin_size_c + ) if checkbox_rect.width() > checkbox_rect.height(): radius = floor(checkbox_rect.height() * 0.5) From 21187bb28cdc76655e562f474b65d6245de449c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 17:13:25 +0100 Subject: [PATCH 525/546] use NoPen instead of transparent --- client/ayon_core/tools/utils/nice_checkbox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/utils/nice_checkbox.py b/client/ayon_core/tools/utils/nice_checkbox.py index b47e3e62e6..3d9d63b6bc 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -383,7 +383,7 @@ class NiceCheckbox(QtWidgets.QFrame): else: radius = floor(checkbox_rect.width() * 0.5) - painter.setPen(QtCore.Qt.transparent) + painter.setPen(QtCore.Qt.NoPen) painter.setBrush(bg_color) painter.drawRoundedRect(checkbox_rect, radius, radius) From cbaefffabc756a7922481263fe12a335467e5d44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Feb 2025 18:55:04 +0100 Subject: [PATCH 526/546] multiselection EnumDef allows empty items --- client/ayon_core/lib/attribute_definitions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index e8327a45b6..f3de4fc943 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -562,17 +562,18 @@ class EnumDef(AbstractAttrDef): multiselection: Optional[bool] = False, **kwargs ): - if not items: - raise ValueError(( - "Empty 'items' value. {} must have" + if multiselection is None: + multiselection = False + + if not items and not multiselection: + raise ValueError( + f"Empty 'items' value. {self.__class__.__name__} must have" " defined values on initialization." - ).format(self.__class__.__name__)) + ) items = self.prepare_enum_items(items) item_values = [item["value"] for item in items] item_values_set = set(item_values) - if multiselection is None: - multiselection = False if multiselection: if default is None: From 8a27bd5bc3a680d144e23c1f4a3f4a08ac3957ae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:34:36 +0100 Subject: [PATCH 527/546] implemented 'PlaceholderPlainTextEdit' --- client/ayon_core/tools/utils/__init__.py | 2 ++ client/ayon_core/tools/utils/widgets.py | 41 +++++++++++++++++++----- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/utils/__init__.py b/client/ayon_core/tools/utils/__init__.py index 4714e76ea3..9206af9beb 100644 --- a/client/ayon_core/tools/utils/__init__.py +++ b/client/ayon_core/tools/utils/__init__.py @@ -5,6 +5,7 @@ from .widgets import ( ComboBox, CustomTextComboBox, PlaceholderLineEdit, + PlaceholderPlainTextEdit, ElideLabel, HintedLineEdit, ExpandingTextEdit, @@ -89,6 +90,7 @@ __all__ = ( "ComboBox", "CustomTextComboBox", "PlaceholderLineEdit", + "PlaceholderPlainTextEdit", "ElideLabel", "HintedLineEdit", "ExpandingTextEdit", diff --git a/client/ayon_core/tools/utils/widgets.py b/client/ayon_core/tools/utils/widgets.py index 059a06648b..1074b6d4fb 100644 --- a/client/ayon_core/tools/utils/widgets.py +++ b/client/ayon_core/tools/utils/widgets.py @@ -88,23 +88,48 @@ class CustomTextComboBox(ComboBox): painter.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, option) -class PlaceholderLineEdit(QtWidgets.QLineEdit): - """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" - def __init__(self, *args, **kwargs): - super(PlaceholderLineEdit, self).__init__(*args, **kwargs) - # Change placeholder palette color - if hasattr(QtGui.QPalette, "PlaceholderText"): - filter_palette = self.palette() +class _Cache: + _placeholder_color = None + + @classmethod + def get_placeholder_color(cls): + if cls._placeholder_color is None: color_obj = get_objected_colors("font") color = color_obj.get_qcolor() color.setAlpha(67) + cls._placeholder_color = color + return cls._placeholder_color + + +class PlaceholderLineEdit(QtWidgets.QLineEdit): + """Set placeholder color of QLineEdit in Qt 5.12 and higher.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): + filter_palette = self.palette() filter_palette.setColor( QtGui.QPalette.PlaceholderText, - color + _Cache.get_placeholder_color() ) self.setPalette(filter_palette) +class PlaceholderPlainTextEdit(QtWidgets.QPlainTextEdit): + """Set placeholder color of QPlainTextEdit in Qt 5.12 and higher.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Change placeholder palette color + if hasattr(QtGui.QPalette, "PlaceholderText"): + viewport = self.viewport() + filter_palette = viewport.palette() + filter_palette.setColor( + QtGui.QPalette.PlaceholderText, + _Cache.get_placeholder_color() + ) + viewport.setPalette(filter_palette) + + class ElideLabel(QtWidgets.QLabel): """Label which elide text. From bfbc8baa3cb5ec684099d61a2a101fedbb30d9c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:34:51 +0100 Subject: [PATCH 528/546] use placeholder widgets in attribute definitions --- client/ayon_core/tools/attribute_defs/widgets.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 201fd5be48..e4f1ddec51 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -22,6 +22,8 @@ from ayon_core.tools.utils import ( FocusSpinBox, FocusDoubleSpinBox, MultiSelectionComboBox, + PlaceholderLineEdit, + PlaceholderPlainTextEdit, set_style_property, ) from ayon_core.tools.utils import NiceCheckbox @@ -502,9 +504,9 @@ class TextAttrWidget(_BaseAttrDefWidget): self.multiline = self.attr_def.multiline if self.multiline: - input_widget = QtWidgets.QPlainTextEdit(self) + input_widget = PlaceholderPlainTextEdit(self) else: - input_widget = QtWidgets.QLineEdit(self) + input_widget = PlaceholderLineEdit(self) # Override context menu event to add revert to default action input_widget.contextMenuEvent = self._input_widget_context_event From 416657eefa0aa5294ea70ebd6a20e14214f55243 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:05:55 +0100 Subject: [PATCH 529/546] added empty item --- .../ayon_core/tools/attribute_defs/widgets.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/attribute_defs/widgets.py b/client/ayon_core/tools/attribute_defs/widgets.py index 93f63730f5..32671eade7 100644 --- a/client/ayon_core/tools/attribute_defs/widgets.py +++ b/client/ayon_core/tools/attribute_defs/widgets.py @@ -2,7 +2,7 @@ import copy import typing from typing import Optional -from qtpy import QtWidgets, QtCore +from qtpy import QtWidgets, QtCore, QtGui from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -655,6 +655,9 @@ class EnumAttrWidget(_BaseAttrDefWidget): for item in self.attr_def.items: input_widget.addItem(item["label"], item["value"]) + if not self.attr_def.items: + self._add_empty_item(input_widget) + idx = input_widget.findData(self.attr_def.default) if idx >= 0: input_widget.setCurrentIndex(idx) @@ -671,6 +674,20 @@ class EnumAttrWidget(_BaseAttrDefWidget): input_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) input_widget.customContextMenuRequested.connect(self._on_context_menu) + def _add_empty_item(self, input_widget): + model = input_widget.model() + if not isinstance(model, QtGui.QStandardItemModel): + return + + root_item = model.invisibleRootItem() + + empty_item = QtGui.QStandardItem() + empty_item.setData("< No items to select >", QtCore.Qt.DisplayRole) + empty_item.setData("", QtCore.Qt.UserRole) + empty_item.setFlags(QtCore.Qt.NoItemFlags) + + root_item.appendRow(empty_item) + def _on_context_menu(self, pos): menu = QtWidgets.QMenu(self) From f0943753c40565dcb1217b571eaba108b0c5b2b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:08:44 +0100 Subject: [PATCH 530/546] set column stretch for pre-create attributes --- client/ayon_core/tools/publisher/widgets/precreate_widget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/publisher/widgets/precreate_widget.py b/client/ayon_core/tools/publisher/widgets/precreate_widget.py index 5ad203d370..b786fea3b5 100644 --- a/client/ayon_core/tools/publisher/widgets/precreate_widget.py +++ b/client/ayon_core/tools/publisher/widgets/precreate_widget.py @@ -85,6 +85,8 @@ class AttributesWidget(QtWidgets.QWidget): layout.setContentsMargins(0, 0, 0, 0) layout.setHorizontalSpacing(INPUTS_LAYOUT_HSPACING) layout.setVerticalSpacing(INPUTS_LAYOUT_VSPACING) + layout.setColumnStretch(0, 0) + layout.setColumnStretch(1, 1) self._layout = layout From c6093f72946898ddff80010f48509bb3a75e429c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:31:28 +0100 Subject: [PATCH 531/546] use task label if is filled --- .../tools/common_models/hierarchy.py | 28 +++++++++++++++---- client/ayon_core/tools/utils/tasks_widget.py | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index 6bccb0f468..323a433a4f 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -1,12 +1,18 @@ +from __future__ import annotations + import time import collections import contextlib +import typing from abc import ABC, abstractmethod import ayon_api from ayon_core.lib import NestedCacheItem +if typing.TYPE_CHECKING: + from typing import Union + HIERARCHY_MODEL_SENDER = "hierarchy.model" @@ -82,19 +88,26 @@ class TaskItem: Args: task_id (str): Task id. name (str): Name of task. + name (Union[str, None]): Task label. task_type (str): Type of task. parent_id (str): Parent folder id. """ def __init__( - self, task_id, name, task_type, parent_id + self, + task_id: str, + name: str, + label: Union[str, None], + task_type: str, + parent_id: str, ): self.task_id = task_id self.name = name + self.label = label self.task_type = task_type self.parent_id = parent_id - self._label = None + self._full_label = None @property def id(self): @@ -107,15 +120,16 @@ class TaskItem: return self.task_id @property - def label(self): + def full_label(self): """Label of task item for UI. Returns: str: Label of task item. """ - if self._label is None: - self._label = "{} ({})".format(self.name, self.task_type) + if self._full_label is None: + label = self.label or self.name + self._full_label = f"{label} ({self.task_type})" return self._label def to_data(self): @@ -128,6 +142,7 @@ class TaskItem: return { "task_id": self.task_id, "name": self.name, + "label": self.label, "parent_id": self.parent_id, "task_type": self.task_type, } @@ -159,6 +174,7 @@ def _get_task_items_from_tasks(tasks): output.append(TaskItem( task["id"], task["name"], + task["label"], task["type"], folder_id )) @@ -368,7 +384,7 @@ class HierarchyModel(object): sender (Union[str, None]): Who requested the task item. Returns: - Union[TaskItem, None]: Task item found by name and folder id. + Optional[TaskItem]: Task item found by name and folder id. """ for task_item in self.get_task_items(project_name, folder_id, sender): diff --git a/client/ayon_core/tools/utils/tasks_widget.py b/client/ayon_core/tools/utils/tasks_widget.py index bba7b93925..87a4c3db3b 100644 --- a/client/ayon_core/tools/utils/tasks_widget.py +++ b/client/ayon_core/tools/utils/tasks_widget.py @@ -270,7 +270,7 @@ class TasksQtModel(QtGui.QStandardItemModel): task_type_item_by_name, task_type_icon_cache ) - item.setData(task_item.label, QtCore.Qt.DisplayRole) + item.setData(task_item.full_label, QtCore.Qt.DisplayRole) item.setData(name, ITEM_NAME_ROLE) item.setData(task_item.id, ITEM_ID_ROLE) item.setData(task_item.task_type, TASK_TYPE_ROLE) From 94a32c0a061f5aef2e851f2eea422c63fa1ca9b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:47:58 +0100 Subject: [PATCH 532/546] fix used variable name --- client/ayon_core/tools/common_models/hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/common_models/hierarchy.py b/client/ayon_core/tools/common_models/hierarchy.py index 323a433a4f..edff8471b0 100644 --- a/client/ayon_core/tools/common_models/hierarchy.py +++ b/client/ayon_core/tools/common_models/hierarchy.py @@ -130,7 +130,7 @@ class TaskItem: if self._full_label is None: label = self.label or self.name self._full_label = f"{label} ({self.task_type})" - return self._label + return self._full_label def to_data(self): """Converts task item to data. From e1b0680ba47540adbc09a7c886806915cf74f4ea Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:58:55 +0100 Subject: [PATCH 533/546] Added basic tests for env parse and compute --- tests/client/ayon_core/lib/test_env_tools.py | 126 +++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 tests/client/ayon_core/lib/test_env_tools.py diff --git a/tests/client/ayon_core/lib/test_env_tools.py b/tests/client/ayon_core/lib/test_env_tools.py new file mode 100644 index 0000000000..396d430376 --- /dev/null +++ b/tests/client/ayon_core/lib/test_env_tools.py @@ -0,0 +1,126 @@ +import platform +import unittest +from unittest.mock import patch + +from ayon_core.lib.env_tools import ( + CycleError, + DynamicKeyClashError, + parse_env_variables_structure, + compute_env_variables_structure, +) + +COMPUTE_SRC_ENV = { + "COMPUTE_VERSION": "1.0.0", + # Will be available only for darwin + "COMPUTE_ONE_PLATFORM": { + "darwin": "Compute macOs", + }, + "COMPUTE_LOCATION": { + "darwin": "/compute-app-{COMPUTE_VERSION}", + "linux": "/usr/compute-app-{COMPUTE_VERSION}", + "windows": "C:/Program Files/compute-app-{COMPUTE_VERSION}" + }, + "PATH_LIST": { + "darwin": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"], + "linux": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"], + "windows": ["{COMPUTE_LOCATION}/bin", "{COMPUTE_LOCATION}/bin2"], + }, + "PATH_STR": { + "darwin": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "linux": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "windows": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2", + }, +} + +PARSE_RESULT_WINDOWS = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "C:/Program Files/compute-app-{COMPUTE_VERSION}", + "PATH_LIST": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2", + "PATH_STR": "{COMPUTE_LOCATION}/bin;{COMPUTE_LOCATION}/bin2", +} + +PARSE_RESULT_LINUX = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "/usr/compute-app-{COMPUTE_VERSION}", + "PATH_LIST": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", +} + +PARSE_RESULT_DARWIN = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_ONE_PLATFORM": "Compute macOs", + "COMPUTE_LOCATION": "/compute-app-{COMPUTE_VERSION}", + "PATH_LIST": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", + "PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", +} + +COMPUTE_RESULT_WINDOWS = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "C:/Program Files/compute-app-1.0.0", + "PATH_LIST": "C:/Program Files/compute-app-1.0.0/bin;C:/Program Files/compute-app-1.0.0/bin2", + "PATH_STR": "C:/Program Files/compute-app-1.0.0/bin;C:/Program Files/compute-app-1.0.0/bin2" +} + +COMPUTE_RESULT_LINUX = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_LOCATION": "/usr/compute-app-1.0.0", + "PATH_LIST": "/usr/compute-app-1.0.0/bin:/usr/compute-app-1.0.0/bin2", + "PATH_STR": "/usr/compute-app-1.0.0/bin:/usr/compute-app-1.0.0/bin2" +} + +COMPUTE_RESULT_DARWIN = { + "COMPUTE_VERSION": "1.0.0", + "COMPUTE_ONE_PLATFORM": "Compute macOs", + "COMPUTE_LOCATION": "/compute-app-1.0.0", + "PATH_LIST": "/compute-app-1.0.0/bin:/compute-app-1.0.0/bin2", + "PATH_STR": "/compute-app-1.0.0/bin:/compute-app-1.0.0/bin2" +} + + +class EnvParseCompute(unittest.TestCase): + def test_parse_env(self): + with patch("platform.system", return_value="windows"): + result = parse_env_variables_structure(COMPUTE_SRC_ENV) + assert result == PARSE_RESULT_WINDOWS + + with patch("platform.system", return_value="linux"): + result = parse_env_variables_structure(COMPUTE_SRC_ENV) + assert result == PARSE_RESULT_LINUX + + with patch("platform.system", return_value="darwin"): + result = parse_env_variables_structure(COMPUTE_SRC_ENV) + assert result == PARSE_RESULT_DARWIN + + def test_compute_env(self): + with patch("platform.system", return_value="windows"): + result = compute_env_variables_structure( + parse_env_variables_structure(COMPUTE_SRC_ENV) + ) + assert result == COMPUTE_RESULT_WINDOWS + + with patch("platform.system", return_value="linux"): + result = compute_env_variables_structure( + parse_env_variables_structure(COMPUTE_SRC_ENV) + ) + assert result == COMPUTE_RESULT_LINUX + + with patch("platform.system", return_value="darwin"): + result = compute_env_variables_structure( + parse_env_variables_structure(COMPUTE_SRC_ENV) + ) + assert result == COMPUTE_RESULT_DARWIN + + def test_cycle_error(self): + with self.assertRaises(CycleError): + compute_env_variables_structure({ + "KEY_1": "{KEY_2}", + "KEY_2": "{KEY_1}", + }) + + def test_dynamic_key_error(self): + with self.assertRaises(DynamicKeyClashError): + compute_env_variables_structure({ + "KEY_A": "Occupied", + "SUBKEY": "A", + "KEY_{SUBKEY}": "Resolves as occupied key", + }) From f28cfe4c0ea013d7e43abfe3bc96f6f5b26c1a2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:59:20 +0100 Subject: [PATCH 534/546] modify code to be able run tests --- client/ayon_core/lib/env_tools.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/env_tools.py b/client/ayon_core/lib/env_tools.py index c1bfe0c292..b02966fac2 100644 --- a/client/ayon_core/lib/env_tools.py +++ b/client/ayon_core/lib/env_tools.py @@ -122,6 +122,9 @@ def parse_env_variables_structure( if platform_name is None: platform_name = platform.system().lower() + # Separator based on OS 'os.pathsep' is ';' on Windows and ':' on Unix + sep = ";" if platform_name == "windows" else ":" + result = {} for variable, value in env.items(): # Platform specific values @@ -130,7 +133,7 @@ def parse_env_variables_structure( # Allow to have lists as values in the tool data if isinstance(value, (list, tuple)): - value = os.pathsep.join(value) + value = sep.join(value) if not value: continue From f09ece485f5c7462b89e88c0cc58ca40269cdad7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:59:49 +0100 Subject: [PATCH 535/546] removed unused import --- tests/client/ayon_core/lib/test_env_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/client/ayon_core/lib/test_env_tools.py b/tests/client/ayon_core/lib/test_env_tools.py index 396d430376..5bc63a6158 100644 --- a/tests/client/ayon_core/lib/test_env_tools.py +++ b/tests/client/ayon_core/lib/test_env_tools.py @@ -1,4 +1,3 @@ -import platform import unittest from unittest.mock import patch From 7e5f9f27d133f75ce8933b11fdde534ad5fc73bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:00:43 +0100 Subject: [PATCH 536/546] added separators --- tests/client/ayon_core/lib/test_env_tools.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/client/ayon_core/lib/test_env_tools.py b/tests/client/ayon_core/lib/test_env_tools.py index 5bc63a6158..7c9ff26d6f 100644 --- a/tests/client/ayon_core/lib/test_env_tools.py +++ b/tests/client/ayon_core/lib/test_env_tools.py @@ -8,6 +8,7 @@ from ayon_core.lib.env_tools import ( compute_env_variables_structure, ) +# --- Test data --- COMPUTE_SRC_ENV = { "COMPUTE_VERSION": "1.0.0", # Will be available only for darwin @@ -31,6 +32,8 @@ COMPUTE_SRC_ENV = { }, } +# --- RESULTS --- +# --- Parse results --- PARSE_RESULT_WINDOWS = { "COMPUTE_VERSION": "1.0.0", "COMPUTE_LOCATION": "C:/Program Files/compute-app-{COMPUTE_VERSION}", @@ -53,6 +56,7 @@ PARSE_RESULT_DARWIN = { "PATH_STR": "{COMPUTE_LOCATION}/bin:{COMPUTE_LOCATION}/bin2", } +# --- Compute results --- COMPUTE_RESULT_WINDOWS = { "COMPUTE_VERSION": "1.0.0", "COMPUTE_LOCATION": "C:/Program Files/compute-app-1.0.0", From 02b22797170ce8a2326864dfa4fd4dc44d696c6f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:06:21 +0100 Subject: [PATCH 537/546] match typehints in arguments --- client/ayon_core/lib/env_tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/lib/env_tools.py b/client/ayon_core/lib/env_tools.py index b02966fac2..bc788a082d 100644 --- a/client/ayon_core/lib/env_tools.py +++ b/client/ayon_core/lib/env_tools.py @@ -63,9 +63,9 @@ def get_paths_from_environ( """Return existing paths from specific environment variable. Args: - env_key (str): Environment key where should look for paths. - env_value (str): Value of environment variable. Argument `env_key` is - skipped if this argument is entered. + env_key (Optional[str]): Environment key where should look for paths. + env_value (Optional[str]): Value of environment variable. + Argument `env_key` is skipped if this argument is entered. return_first (bool): Return first found value or return list of found paths. `None` or empty list returned if nothing found. From 17399b8f4a6b2cb53db950fd2cccd477307346ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:06:47 +0100 Subject: [PATCH 538/546] fix formatting --- tests/client/ayon_core/lib/test_env_tools.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/client/ayon_core/lib/test_env_tools.py b/tests/client/ayon_core/lib/test_env_tools.py index 7c9ff26d6f..38aac822bf 100644 --- a/tests/client/ayon_core/lib/test_env_tools.py +++ b/tests/client/ayon_core/lib/test_env_tools.py @@ -60,8 +60,14 @@ PARSE_RESULT_DARWIN = { COMPUTE_RESULT_WINDOWS = { "COMPUTE_VERSION": "1.0.0", "COMPUTE_LOCATION": "C:/Program Files/compute-app-1.0.0", - "PATH_LIST": "C:/Program Files/compute-app-1.0.0/bin;C:/Program Files/compute-app-1.0.0/bin2", - "PATH_STR": "C:/Program Files/compute-app-1.0.0/bin;C:/Program Files/compute-app-1.0.0/bin2" + "PATH_LIST": ( + "C:/Program Files/compute-app-1.0.0/bin" + ";C:/Program Files/compute-app-1.0.0/bin2" + ), + "PATH_STR": ( + "C:/Program Files/compute-app-1.0.0/bin" + ";C:/Program Files/compute-app-1.0.0/bin2" + ) } COMPUTE_RESULT_LINUX = { From b09bcdada8e6f842eee0a4cd202f102607d0c1c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:17:00 +0100 Subject: [PATCH 539/546] remove trailing spaces --- tests/client/ayon_core/lib/test_env_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/client/ayon_core/lib/test_env_tools.py b/tests/client/ayon_core/lib/test_env_tools.py index 38aac822bf..e7aea7fd7d 100644 --- a/tests/client/ayon_core/lib/test_env_tools.py +++ b/tests/client/ayon_core/lib/test_env_tools.py @@ -58,7 +58,7 @@ PARSE_RESULT_DARWIN = { # --- Compute results --- COMPUTE_RESULT_WINDOWS = { - "COMPUTE_VERSION": "1.0.0", + "COMPUTE_VERSION": "1.0.0", "COMPUTE_LOCATION": "C:/Program Files/compute-app-1.0.0", "PATH_LIST": ( "C:/Program Files/compute-app-1.0.0/bin" From a78ee95070cac951c90ef4d3d727de8e656424a5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 17 Feb 2025 10:51:12 +0000 Subject: [PATCH 540/546] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 909ecd7a3c..a93ec35297 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.1.0+dev" +__version__ = "1.1.1" diff --git a/package.py b/package.py index 0b888f5c33..a33fc9d77c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.0+dev" +version = "1.1.1" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 32822391c8..bfe36e9f5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.1.0+dev" +version = "1.1.1" description = "" authors = ["Ynput Team "] readme = "README.md" From a83f1ca5ade16afb5a14de4c5150dfdde6ea089f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 17 Feb 2025 10:51:52 +0000 Subject: [PATCH 541/546] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a93ec35297..f2e82af12b 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.1.1" +__version__ = "1.1.1+dev" diff --git a/package.py b/package.py index a33fc9d77c..b9629d6c51 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.1" +version = "1.1.1+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index bfe36e9f5a..87fe9708dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.1.1" +version = "1.1.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 777e03d88456acb0e5a6638ce34d70c52943112c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:33:13 +0100 Subject: [PATCH 542/546] add required dependencies for running tests --- poetry.lock | 712 ++++++++++++++++++++++++++++++------------------- pyproject.toml | 9 +- 2 files changed, 445 insertions(+), 276 deletions(-) diff --git a/poetry.lock b/poetry.lock index be5a3b2c2c..2d040a5f91 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "appdirs" @@ -6,37 +6,59 @@ version = "1.4.4" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, ] +[[package]] +name = "attrs" +version = "25.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, +] + +[package.extras] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] + [[package]] name = "ayon-python-api" -version = "1.0.1" +version = "1.0.12" description = "AYON Python API" optional = false python-versions = "*" +groups = ["dev"] files = [ - {file = "ayon-python-api-1.0.1.tar.gz", hash = "sha256:6a53af84903317e2097f3c6bba0094e90d905d6670fb9c7d3ad3aa9de6552bc1"}, - {file = "ayon_python_api-1.0.1-py3-none-any.whl", hash = "sha256:d4b649ac39c9003cdbd60f172c0d35f05d310fba3a0649b6d16300fe67f967d6"}, + {file = "ayon-python-api-1.0.12.tar.gz", hash = "sha256:8e4c03436df8afdda4c6ad4efce436068771995bb0153a90e003364afa0e7f55"}, + {file = "ayon_python_api-1.0.12-py3-none-any.whl", hash = "sha256:65f61c2595dd6deb26fed5e3fda7baef887f475fa4b21df12513646ddccf4a7d"}, ] [package.dependencies] appdirs = ">=1,<2" requests = ">=2.27.1" -six = ">=1.15" -Unidecode = ">=1.2.0" +Unidecode = ">=1.3.0" [[package]] name = "certifi" -version = "2024.2.2" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -45,6 +67,7 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -52,118 +75,139 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] +[[package]] +name = "clique" +version = "2.0.0" +description = "Manage collections with common numerical component" +optional = false +python-versions = ">=3.0, <4.0" +groups = ["dev"] +files = [ + {file = "clique-2.0.0-py2.py3-none-any.whl", hash = "sha256:45e2a4c6078382e0b217e5e369494279cf03846d95ee601f93290bed5214c22e"}, + {file = "clique-2.0.0.tar.gz", hash = "sha256:6e1115dbf21b1726f4b3db9e9567a662d6bdf72487c4a0a1f8cb7f10cf4f4754"}, +] + +[package.extras] +dev = ["lowdown (>=0.2.0,<1)", "pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] +doc = ["lowdown (>=0.2.0,<1)", "sphinx (>=2,<4)", "sphinx-rtd-theme (>=0.1.6,<1)"] +test = ["pytest (>=2.3.5,<5)", "pytest-cov (>=2,<3)", "pytest-runner (>=2.7,<3)"] + [[package]] name = "codespell" -version = "2.2.6" -description = "Codespell" +version = "2.4.1" +description = "Fix common misspellings in text files" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "codespell-2.2.6-py3-none-any.whl", hash = "sha256:9ee9a3e5df0990604013ac2a9f22fa8e57669c827124a2e961fe8a1da4cacc07"}, - {file = "codespell-2.2.6.tar.gz", hash = "sha256:a8c65d8eb3faa03deabab6b3bbe798bea72e1799c7e9e955d57eca4096abcff9"}, + {file = "codespell-2.4.1-py3-none-any.whl", hash = "sha256:3dadafa67df7e4a3dbf51e0d7315061b80d265f9552ebd699b3dd6834b47e425"}, + {file = "codespell-2.4.1.tar.gz", hash = "sha256:299fcdcb09d23e81e35a671bbe746d5ad7e8385972e65dbb833a2eaac33c01e5"}, ] [package.extras] dev = ["Pygments", "build", "chardet", "pre-commit", "pytest", "pytest-cov", "pytest-dependency", "ruff", "tomli", "twine"] hard-encoding-detection = ["chardet"] -toml = ["tomli"] +toml = ["tomli ; python_version < \"3.11\""] types = ["chardet (>=5.1.0)", "mypy", "pytest", "pytest-cov", "pytest-dependency"] [[package]] @@ -172,6 +216,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -179,24 +225,26 @@ files = [ [[package]] name = "distlib" -version = "0.3.8" +version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -204,29 +252,31 @@ test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.13.1" +version = "3.17.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, - {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] -typing = ["typing-extensions (>=4.8)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] [[package]] name = "identify" -version = "2.5.35" +version = "2.6.7" description = "File identification library for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, - {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, + {file = "identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0"}, + {file = "identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684"}, ] [package.extras] @@ -234,75 +284,149 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.6" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +groups = ["dev"] files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] -name = "nodeenv" -version = "1.8.0" -description = "Node.js virtual environment builder" +name = "mock" +version = "5.1.0" +description = "Rolling backport of unittest.mock for all Pythons" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +python-versions = ">=3.6" +groups = ["dev"] files = [ - {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, - {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, + {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, + {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, ] -[package.dependencies] -setuptools = "*" +[package.extras] +build = ["blurb", "twine", "wheel"] +docs = ["sphinx"] +test = ["pytest", "pytest-cov"] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "opentimelineio" +version = "0.17.0" +description = "Editorial interchange format and API" +optional = false +python-versions = "!=3.9.0,>=3.7" +groups = ["dev"] +files = [ + {file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:2dd31a570cabfd6227c1b1dd0cc038da10787492c26c55de058326e21fe8a313"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a1da5d4803d1ba5e846b181a9e0f4a392c76b9acc5e08947772bc086f2ebfc0"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3527977aec8202789a42d60e1e0dc11b4154f585ef72921760445f43e7967a00"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3aafb4c50455832ed2627c2cac654b896473a5c1f8348ddc07c10be5cfbd59"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-win32.whl", hash = "sha256:fee45af9f6330773893cd0858e92f8256bb5bde4229b44a76f03e59a9fb1b1b6"}, + {file = "OpenTimelineIO-0.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:d51887619689c21d67cc4b11b1088f99ae44094513315e7a144be00f1393bfa8"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:cbf05c3e8c0187969f79e91f7495d1f0dc3609557874d8e601ba2e072c70ddb1"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d3430c3f4e88c5365d7b6afbee920b0815b62ecf141abe44cd739c9eedc04284"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1912345227b0bd1654c7153863eadbcee60362aa46340678e576e5d2aa3106a"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51e06eb11a868d970c1534e39faf916228d5163bf3598076d408d8f393ab0bd4"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-win32.whl", hash = "sha256:5c3a3f4780b25a8c1a80d788becba691d12b629069ad8783d0db21027639276f"}, + {file = "OpenTimelineIO-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:43c8726b33af30ba42928972192311ea0f986edbbd5f74651bada182d4fe805c"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:9a9af4105a088c0ab131780e49db268db7e37871aac33db842de6b2b16f14e39"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e653ad1dd3b85f5c312a742dc24b61b330964aa391dc5bc072fe8b9c85adff1"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:02a77823c27a1b93c6b87682372c3734ac5fddc10bfe53875e657d43c60fb885"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4f4efcf3ddd81b62c4feb49a0bcc309b50ffeb6a8c48ab173d169a029006f4d"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-win32.whl", hash = "sha256:9872ab74a20bb2bb3a50af04e80fe9238998d67d6be4e30e45aebe25d3eefac6"}, + {file = "OpenTimelineIO-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:c83b78be3312d3152d7e07ab32b0086fe220acc2a5b035b70ad69a787c0ece62"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:0e671a6f2a1f772445bb326c7640dc977cfc3db589fe108a783a0311939cfac8"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b931a3189b4ce064f06f15a89fe08ef4de01f7dcf0abc441fe2e02ef2a3311bb"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:923cb54d806c981cf1e91916c3e57fba5664c22f37763dd012bad5a5a7bd4db4"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-win32.whl", hash = "sha256:8e16598c5084dcb21df3d83978b0e5f72300af9edd4cdcb85e3b0ba5da0df4e8"}, + {file = "OpenTimelineIO-0.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7eed5033494888fb3f802af50e60559e279b2f398802748872903c2f54efd2c9"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:118baa22b9227da5003bee653601a68686ae2823682dcd7d13c88178c63081c3"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:43389eacdee2169de454e1c79ecfea82f54a9e73b67151427a9b621349a22b7f"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:17659b1e6aa42ed617a942f7a2bfc6ecc375d0464ec127ce9edf896278ecaee9"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d5ea8cfbebf3c9013cc680eef5be48bffb515aafa9dc31e99bf66052a4ca3d"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-win32.whl", hash = "sha256:cc67c74eb4b73bc0f7d135d3ff3dbbd86b2d451a9b142690a8d1631ad79c46f2"}, + {file = "OpenTimelineIO-0.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:69b39079bee6fa4aff34c6ad6544df394bc7388483fa5ce958ecd16e243a53ad"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:a33554894dea17c22feec0201991e705c2c90a679ba2a012a0c558a7130df711"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6b1ad3b3155370245b851b2f7b60006b2ebbb5bb76dd0fdc49bb4dce73fa7d96"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:030454a9c0e9e82e5a153119f9afb8f3f4e64a3b27f80ac0dcde44b029fd3f3f"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce64376a28919533bd4f744ff8885118abefa73f78fd408f95fa7a9489855b6"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-win32.whl", hash = "sha256:fa8cdceb25f9003c3c0b5b32baef2c764949d88b867161ddc6f44f48f6bbfa4a"}, + {file = "OpenTimelineIO-0.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fbcf8a000cd688633c8dc5d22e91912013c67c674329eba603358e3b54da32bf"}, + {file = "opentimelineio-0.17.0.tar.gz", hash = "sha256:10ef324e710457e9977387cd9ef91eb24a9837bfb370aec3330f9c0f146cea85"}, +] + +[package.extras] +dev = ["check-manifest", "coverage (>=4.5)", "flake8 (>=3.5)", "urllib3 (>=1.24.3)"] +view = ["PySide2 (>=5.11,<6.0) ; platform_machine == \"x86_64\"", "PySide6 (>=6.2,<7.0) ; platform_machine == \"aarch64\""] [[package]] name = "packaging" -version = "24.0" +version = "24.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.3.6" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] @@ -311,13 +435,14 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.6.2" +version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, - {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] @@ -327,15 +452,28 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pyblish-base" +version = "1.8.12" +description = "Plug-in driven automation framework for content" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pyblish-base-1.8.12.tar.gz", hash = "sha256:ebc184eb038864380555227a8b58055dd24ece7e6ef7f16d33416c718512871b"}, + {file = "pyblish_base-1.8.12-py2.py3-none-any.whl", hash = "sha256:2cbe956bfbd4175a2d7d22b344cd345800f4d4437153434ab658fc12646a11e8"}, +] + [[package]] name = "pytest" -version = "8.1.1" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -343,97 +481,103 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-print" -version = "1.0.0" +version = "1.0.2" description = "pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout)" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "pytest_print-1.0.0-py3-none-any.whl", hash = "sha256:23484f42b906b87e31abd564761efffeb0348a6f83109fb857ee6e8e5df42b69"}, - {file = "pytest_print-1.0.0.tar.gz", hash = "sha256:1fcde9945fba462227a8959271369b10bb7a193be8452162707e63cd60875ca0"}, + {file = "pytest_print-1.0.2-py3-none-any.whl", hash = "sha256:3ae7891085dddc3cd697bd6956787240107fe76d6b5cdcfcd782e33ca6543de9"}, + {file = "pytest_print-1.0.2.tar.gz", hash = "sha256:2780350a7bbe7117f99c5d708dc7b0431beceda021b1fd3f11200670d7f33679"}, ] [package.dependencies] -pytest = ">=7.4" +pytest = ">=8.3.2" [package.extras] -test = ["covdefaults (>=2.3)", "coverage (>=7.3)", "pytest-mock (>=3.11.1)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "requests" -version = "2.31.0" +version = "2.32.3" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] @@ -448,66 +592,83 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.3.3" +version = "0.3.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, - {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, - {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, - {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, - {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, - {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, - {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, - {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, + {file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, + {file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, + {file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, + {file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, + {file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, + {file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, + {file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, ] [[package]] -name = "setuptools" -version = "69.2.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +name = "semver" +version = "3.0.4" +description = "Python helper for Semantic Versioning (https://semver.org)" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, -] - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, + {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, ] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -516,6 +677,7 @@ version = "1.3.8" description = "ASCII transliterations of Unicode text" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"}, {file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"}, @@ -523,30 +685,32 @@ files = [ [[package]] name = "urllib3" -version = "2.2.1" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, - {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.25.1" +version = "20.29.2" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, - {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, + {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, + {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, ] [package.dependencies] @@ -555,10 +719,10 @@ filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.9.1,<3.10" -content-hash = "1bb724694792fbc2b3c05e3355e6c25305d9f4034eb7b1b4b1791ee95427f8d2" +content-hash = "0a399d239c49db714c1166c20286fdd5cd62faf12e45ab85833c4d6ea7a04a2a" diff --git a/pyproject.toml b/pyproject.toml index 87fe9708dc..9833902c16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,12 +9,12 @@ version = "1.1.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md" +package-mode = false [tool.poetry.dependencies] python = ">=3.9.1,<3.10" - -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] # test dependencies pytest = "^8.0" pytest-print = "^1.0" @@ -24,6 +24,11 @@ ruff = "^0.3.3" pre-commit = "^3.6.2" codespell = "^2.2.6" semver = "^3.0.2" +mock = "^5.0.0" +attrs = "^25.0.0" +pyblish-base = "^1.8.7" +clique = "^2.0.0" +opentimelineio = "^0.17.0" [tool.ruff] From ea905daeca9b9b35b75329d263adef37cbb74449 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Feb 2025 14:38:01 +0100 Subject: [PATCH 543/546] added commands to run tests --- tools/manage.ps1 | 11 +++++++++++ tools/manage.sh | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/tools/manage.ps1 b/tools/manage.ps1 index 9a9a9a2eff..8324277713 100755 --- a/tools/manage.ps1 +++ b/tools/manage.ps1 @@ -240,6 +240,13 @@ function Run-From-Code { & $Poetry $RunArgs @arguments } +function Run-Tests { + $Poetry = "$RepoRoot\.poetry\bin\poetry.exe" + $RunArgs = @( "run", "pytest", "$($RepoRoot)/tests") + + & $Poetry $RunArgs @arguments +} + function Write-Help { <# .SYNOPSIS @@ -256,6 +263,7 @@ function Write-Help { Write-Info -Text " ruff-fix ", "Run Ruff fix for the repository" -Color White, Cyan Write-Info -Text " codespell ", "Run codespell check for the repository" -Color White, Cyan Write-Info -Text " run ", "Run a poetry command in the repository environment" -Color White, Cyan + Write-Info -Text " run-tests ", "Run ayon-core tests" -Color White, Cyan Write-Host "" } @@ -280,6 +288,9 @@ function Resolve-Function { } elseif ($FunctionName -eq "run") { Set-Cwd Run-From-Code + } elseif ($FunctionName -eq "runtests") { + Set-Cwd + Run-Tests } else { Write-Host "Unknown function ""$FunctionName""" Write-Help diff --git a/tools/manage.sh b/tools/manage.sh index 6b0a4d6978..86ae7155c5 100755 --- a/tools/manage.sh +++ b/tools/manage.sh @@ -158,6 +158,7 @@ default_help() { echo -e " ${BWhite}ruff-fix${RST} ${BCyan}Run Ruff fix for the repository${RST}" echo -e " ${BWhite}codespell${RST} ${BCyan}Run codespell check for the repository${RST}" echo -e " ${BWhite}run${RST} ${BCyan}Run a poetry command in the repository environment${RST}" + echo -e " ${BWhite}run-tests${RST} ${BCyan}Run ayon-core tests${RST}" echo "" } @@ -182,6 +183,12 @@ run_command () { "$POETRY_HOME/bin/poetry" run "$@" } +run_tests () { + echo -e "${BIGreen}>>>${RST} Running tests..." + shift; # will remove first arg ("run-tests") from the "$@" + "$POETRY_HOME/bin/poetry" run pytest ./tests +} + main () { detect_python || return 1 @@ -218,6 +225,10 @@ main () { run_command "$@" || return_code=$? exit $return_code ;; + "runtests") + run_tests "$@" || return_code=$? + exit $return_code + ;; esac if [ "$function_name" != "" ]; then From 8c8205a4e6406e07b131433ce00c1fb3014cb85c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Feb 2025 16:11:51 +0100 Subject: [PATCH 544/546] Fix doubled quotes around outer elements --- .../plugins/publish/collect_anatomy_instance_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index 354d877b62..a86ef3f24c 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -116,10 +116,10 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): if not_found_folder_paths: joined_folder_paths = ", ".join( - ["\"{}\"".format(path) for path in not_found_folder_paths] + [f"\"{path}\"" for path in not_found_folder_paths] ) self.log.warning(( - "Not found folder entities with paths \"{}\"." + "Not found folder entities with paths {}." ).format(joined_folder_paths)) def fill_missing_task_entities(self, context, project_name): From 5564863371d896f60cf8459ffeeed7f7f5848c45 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Feb 2025 16:12:15 +0100 Subject: [PATCH 545/546] Cosmetics --- .../plugins/publish/collect_anatomy_instance_data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index a86ef3f24c..677ebb04a2 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -118,9 +118,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): joined_folder_paths = ", ".join( [f"\"{path}\"" for path in not_found_folder_paths] ) - self.log.warning(( - "Not found folder entities with paths {}." - ).format(joined_folder_paths)) + self.log.warning( + f"Not found folder entities with paths {joined_folder_paths}." + ) def fill_missing_task_entities(self, context, project_name): self.log.debug("Querying task entities for instances.") From 279df284ff390e9a5458ebf3da9274bf3c992067 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Feb 2025 16:22:36 +0100 Subject: [PATCH 546/546] Change to debug log, because it's not very nice artist-facing information --- client/ayon_core/plugins/publish/extract_otio_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 2461195b27..7a9a020ff0 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -286,7 +286,7 @@ class ExtractOTIOReview( ) instance.data["representations"].append(representation) - self.log.info("Adding representation: {}".format(representation)) + self.log.debug("Adding representation: {}".format(representation)) def _create_representation(self, start, duration): """