From 1de8e300c78850a59d028104139c8bd93e834a96 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 11 Feb 2025 20:07:16 +0200 Subject: [PATCH 01/26] fix a typo --- client/ayon_core/pipeline/context_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index b9ae906ab4..386c7ba737 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -251,7 +251,7 @@ def uninstall_host(): pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) deregister_loader_plugin_path(LOAD_PATH) deregister_inventory_action_path(INVENTORY_PATH) - log.info("Global plug-ins unregistred") + log.info("Global plug-ins unregistered") deregister_host() From bba0df9c0eac4e92b2eb2852736bfd183603657c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 11 Feb 2025 20:07:37 +0200 Subject: [PATCH 02/26] don't `version_up` twice --- client/ayon_core/pipeline/context_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 386c7ba737..a809261fa2 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -617,7 +617,7 @@ def version_up_current_workfile(): last_workfile_path = get_last_workfile( work_root, file_template, data, extensions, True ) - new_workfile_path = version_up(last_workfile_path) + new_workfile_path = last_workfile_path if os.path.exists(new_workfile_path): new_workfile_path = version_up(new_workfile_path) host.save_workfile(new_workfile_path) From 99f60e10125489c4863f1d905fef9c1c6644c10e Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 11 Feb 2025 20:28:03 +0200 Subject: [PATCH 03/26] replace `os.listdir` by `os.scandir` --- client/ayon_core/lib/path_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index 31baac168c..3826f1383e 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -136,8 +136,8 @@ def version_up(filepath): index += len(new_label) clash_basename = clash_basename[:index] - for file in os.listdir(dirname): - if file.endswith(ext) and file.startswith(clash_basename): + for file in os.scandir(dirname): + if file.name.endswith(ext) and file.name.startswith(clash_basename): log.info("Skipping existing version %s" % new_label) return version_up(new_filename) From 9791f591f579e3f7106f1616293f0e034f66cd8a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Tue, 11 Feb 2025 20:30:26 +0200 Subject: [PATCH 04/26] use `f` instead of `file` --- client/ayon_core/lib/path_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index 3826f1383e..f3c35d998b 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -136,8 +136,8 @@ def version_up(filepath): index += len(new_label) clash_basename = clash_basename[:index] - for file in os.scandir(dirname): - if file.name.endswith(ext) and file.name.startswith(clash_basename): + for f in os.scandir(dirname): + if f.name.endswith(ext) and f.name.startswith(clash_basename): log.info("Skipping existing version %s" % new_label) return version_up(new_filename) From da4ebf8eaead85285b7ae985db2788d162c82ae4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:42:31 +0100 Subject: [PATCH 05/26] use env variables to fill root values --- client/ayon_core/pipeline/anatomy/roots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index 2773559d49..71f842d9e0 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -25,7 +25,7 @@ class RootItem(FormatObject): self._log = None lowered_platform_keys = {} for key, value in root_raw_data.items(): - lowered_platform_keys[key.lower()] = value + lowered_platform_keys[key.lower()] = value.format(**os.environ) self.raw_data = lowered_platform_keys self.cleaned_data = self._clean_roots(lowered_platform_keys) self.name = name From 2615e650cb2715799af9458a5b9ef16ff8b22455 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 14:42:45 +0100 Subject: [PATCH 06/26] fill only platform root --- client/ayon_core/pipeline/anatomy/roots.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index 71f842d9e0..1c37e82f08 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -23,16 +23,19 @@ class RootItem(FormatObject): def __init__(self, parent, root_raw_data, name): super(RootItem, self).__init__() self._log = None - lowered_platform_keys = {} - for key, value in root_raw_data.items(): - lowered_platform_keys[key.lower()] = value.format(**os.environ) + lowered_platform_keys = { + key.lower(): value + for key, value in root_raw_data.items() + } self.raw_data = lowered_platform_keys self.cleaned_data = self._clean_roots(lowered_platform_keys) self.name = name self.parent = parent self.available_platforms = set(lowered_platform_keys.keys()) - self.value = lowered_platform_keys.get(platform.system().lower()) + self.value = lowered_platform_keys[platform.system().lower()].format( + **os.environ + ) self.clean_value = self._clean_root(self.value) def __format__(self, *args, **kwargs): @@ -105,10 +108,10 @@ class RootItem(FormatObject): def _clean_roots(self, raw_data): """Clean all values of raw root item values.""" - cleaned = {} - for key, value in raw_data.items(): - cleaned[key] = self._clean_root(value) - return cleaned + return { + key: self._clean_root(value) + for key, value in raw_data.items() + } def path_remapper(self, path, dst_platform=None, src_platform=None): """Remap path for specific platform. From 5a6501aa0861f1eecac9d057085f3953a7416eb0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:03:18 +0100 Subject: [PATCH 07/26] use 'format_map' --- client/ayon_core/pipeline/anatomy/roots.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index 1c37e82f08..128834dcbc 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -33,8 +33,10 @@ class RootItem(FormatObject): self.parent = parent self.available_platforms = set(lowered_platform_keys.keys()) - self.value = lowered_platform_keys[platform.system().lower()].format( - **os.environ + + current_platform = platform.system().lower() + self.value = lowered_platform_keys[current_platform].format_map( + os.environ ) self.clean_value = self._clean_root(self.value) From cee3219304697cc3852e167650b7c40e87912a95 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Sat, 15 Feb 2025 00:49:22 +0200 Subject: [PATCH 08/26] revert using `os.scandir` inside `version_up` --- client/ayon_core/lib/path_tools.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index f3c35d998b..31baac168c 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -136,8 +136,8 @@ def version_up(filepath): index += len(new_label) clash_basename = clash_basename[:index] - for f in os.scandir(dirname): - if f.name.endswith(ext) and f.name.startswith(clash_basename): + for file in os.listdir(dirname): + if file.endswith(ext) and file.startswith(clash_basename): log.info("Skipping existing version %s" % new_label) return version_up(new_filename) From 65d7404d428229e8573169040cb5fec4b5f4e522 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Sat, 15 Feb 2025 01:03:34 +0200 Subject: [PATCH 09/26] add a note about `get_last_workfile` --- client/ayon_core/pipeline/context_tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index a809261fa2..beb62755f5 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -617,6 +617,9 @@ def version_up_current_workfile(): last_workfile_path = get_last_workfile( work_root, file_template, data, extensions, True ) + # `get_last_workfile` will return the first expected file version + # if no files exist yet. In that case, if they do not exist we will + # want to save v001 new_workfile_path = last_workfile_path if os.path.exists(new_workfile_path): new_workfile_path = version_up(new_workfile_path) From 817e4dc3380ba66e93465cf28262f8f96eb07a1f Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Sat, 15 Feb 2025 01:08:52 +0200 Subject: [PATCH 10/26] `version_up_current_workfile`: Raise error if parent folder of the workfile doesn't exist. --- client/ayon_core/pipeline/context_tools.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index beb62755f5..a2732d2c55 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -623,4 +623,12 @@ def version_up_current_workfile(): new_workfile_path = last_workfile_path if os.path.exists(new_workfile_path): new_workfile_path = version_up(new_workfile_path) + + # Raise an error if the parent folder doesn't exist as `host.save_workfile` + # is not supposed/able to create missing folders. + parent_folder = os.path.dirname(new_workfile_path) + if not os.path.exists(parent_folder): + raise AssertionError( + f"Folder {parent_folder} does not exist yet.") + host.save_workfile(new_workfile_path) From 26acd0efad991f8296c142f682dc3f11b450d6be Mon Sep 17 00:00:00 2001 From: Mustafa Jafar Date: Thu, 27 Feb 2025 15:24:16 +0200 Subject: [PATCH 11/26] update log message Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/context_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index a2732d2c55..3951a4d6e9 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -629,6 +629,6 @@ def version_up_current_workfile(): parent_folder = os.path.dirname(new_workfile_path) if not os.path.exists(parent_folder): raise AssertionError( - f"Folder {parent_folder} does not exist yet.") + f"Work area directory '{parent_folder}' does not exist yet.") host.save_workfile(new_workfile_path) From 1f571e65c168eb0f481ce7704638082b46fafaa1 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 7 Mar 2025 14:17:13 +0200 Subject: [PATCH 12/26] implement `MissingWorkdirError` --- client/ayon_core/pipeline/context_tools.py | 5 +++-- client/ayon_core/pipeline/workfile/__init__.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 3951a4d6e9..2f002ae2c8 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -27,7 +27,8 @@ from .workfile import ( get_workdir, get_custom_workfile_template_by_string_context, get_workfile_template_key_from_context, - get_last_workfile + get_last_workfile, + MissingWorkdirError, ) from . import ( register_loader_plugin_path, @@ -628,7 +629,7 @@ def version_up_current_workfile(): # is not supposed/able to create missing folders. parent_folder = os.path.dirname(new_workfile_path) if not os.path.exists(parent_folder): - raise AssertionError( + raise MissingWorkdirError( f"Work area directory '{parent_folder}' does not exist yet.") host.save_workfile(new_workfile_path) diff --git a/client/ayon_core/pipeline/workfile/__init__.py b/client/ayon_core/pipeline/workfile/__init__.py index 05f939024c..aa7e150bca 100644 --- a/client/ayon_core/pipeline/workfile/__init__.py +++ b/client/ayon_core/pipeline/workfile/__init__.py @@ -16,6 +16,7 @@ from .path_resolving import ( from .utils import ( should_use_last_workfile_on_launch, should_open_workfiles_tool_on_launch, + MissingWorkdirError, ) from .build_workfile import BuildWorkfile @@ -46,6 +47,7 @@ __all__ = ( "should_use_last_workfile_on_launch", "should_open_workfiles_tool_on_launch", + "MissingWorkdirError", "BuildWorkfile", From 8f45e20c2ac5d7d3a1ed58f3bda62faf9023d499 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 7 Mar 2025 14:17:37 +0200 Subject: [PATCH 13/26] use `MissingWorkdirError` --- client/ayon_core/pipeline/workfile/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/workfile/utils.py b/client/ayon_core/pipeline/workfile/utils.py index 53de3269b2..1a5ecdff4a 100644 --- a/client/ayon_core/pipeline/workfile/utils.py +++ b/client/ayon_core/pipeline/workfile/utils.py @@ -2,6 +2,10 @@ from ayon_core.lib import filter_profiles from ayon_core.settings import get_project_settings +class MissingWorkdirError(Exception): + pass + + def should_use_last_workfile_on_launch( project_name, host_name, From bfb24435ab24beaa66e810113088c3c833b1c2bf Mon Sep 17 00:00:00 2001 From: Mustafa Jafar Date: Sun, 9 Mar 2025 01:19:30 +0200 Subject: [PATCH 14/26] Update log message Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/context_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 2f002ae2c8..66556bbb35 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -630,6 +630,6 @@ def version_up_current_workfile(): parent_folder = os.path.dirname(new_workfile_path) if not os.path.exists(parent_folder): raise MissingWorkdirError( - f"Work area directory '{parent_folder}' does not exist yet.") + f"Work area directory '{parent_folder}' does not exist.") host.save_workfile(new_workfile_path) From c05c5045246aedfa20d662bee9e87a0ea814291d Mon Sep 17 00:00:00 2001 From: Mustafa Jafar Date: Sun, 9 Mar 2025 01:20:41 +0200 Subject: [PATCH 15/26] add doc string to `MissingWorkdirError` Co-authored-by: Roy Nieterau --- client/ayon_core/pipeline/workfile/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/workfile/utils.py b/client/ayon_core/pipeline/workfile/utils.py index 1a5ecdff4a..25be061dec 100644 --- a/client/ayon_core/pipeline/workfile/utils.py +++ b/client/ayon_core/pipeline/workfile/utils.py @@ -3,6 +3,7 @@ from ayon_core.settings import get_project_settings class MissingWorkdirError(Exception): + """Raised when accessing a work directory not found on disk.""" pass From 9887b069ff1bebe2b69c757aaf4ed1df77c243d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Apr 2025 15:44:57 +0200 Subject: [PATCH 16/26] add a warning to roots --- client/ayon_core/pipeline/anatomy/roots.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index 128834dcbc..c7b9ff8ebc 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -35,6 +35,9 @@ class RootItem(FormatObject): self.available_platforms = set(lowered_platform_keys.keys()) current_platform = platform.system().lower() + # WARNING: Using environment variables in roots is not considered + # as production safe. Some features may not work as expected, for + # example USD resolver or site sync. self.value = lowered_platform_keys[current_platform].format_map( os.environ ) From 345840bb1c28cd34f433ec1465a74ed12a886560 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:06:37 +0200 Subject: [PATCH 17/26] added exception for missing environment variable --- client/ayon_core/pipeline/anatomy/__init__.py | 2 ++ .../ayon_core/pipeline/anatomy/exceptions.py | 5 +++++ client/ayon_core/pipeline/anatomy/roots.py | 22 ++++++++++++++----- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/anatomy/__init__.py b/client/ayon_core/pipeline/anatomy/__init__.py index 336d09ccaa..7000f51495 100644 --- a/client/ayon_core/pipeline/anatomy/__init__.py +++ b/client/ayon_core/pipeline/anatomy/__init__.py @@ -1,5 +1,6 @@ from .exceptions import ( ProjectNotSet, + RootMissingEnv, RootCombinationError, TemplateMissingKey, AnatomyTemplateUnsolved, @@ -9,6 +10,7 @@ from .anatomy import Anatomy __all__ = ( "ProjectNotSet", + "RootMissingEnv", "RootCombinationError", "TemplateMissingKey", "AnatomyTemplateUnsolved", diff --git a/client/ayon_core/pipeline/anatomy/exceptions.py b/client/ayon_core/pipeline/anatomy/exceptions.py index 39f116baf0..24df0e3046 100644 --- a/client/ayon_core/pipeline/anatomy/exceptions.py +++ b/client/ayon_core/pipeline/anatomy/exceptions.py @@ -5,6 +5,11 @@ class ProjectNotSet(Exception): """Exception raised when is created Anatomy without project name.""" +class RootMissingEnv(KeyError): + """Raised when root requires environment variables which is not filled.""" + pass + + class RootCombinationError(Exception): """This exception is raised when templates has combined root types.""" diff --git a/client/ayon_core/pipeline/anatomy/roots.py b/client/ayon_core/pipeline/anatomy/roots.py index c7b9ff8ebc..bd09a9fe51 100644 --- a/client/ayon_core/pipeline/anatomy/roots.py +++ b/client/ayon_core/pipeline/anatomy/roots.py @@ -2,9 +2,11 @@ import os import platform import numbers -from ayon_core.lib import Logger +from ayon_core.lib import Logger, StringTemplate from ayon_core.lib.path_templates import FormatObject +from .exceptions import RootMissingEnv + class RootItem(FormatObject): """Represents one item or roots. @@ -21,7 +23,7 @@ class RootItem(FormatObject): multi root setup otherwise None value is expected. """ def __init__(self, parent, root_raw_data, name): - super(RootItem, self).__init__() + super().__init__() self._log = None lowered_platform_keys = { key.lower(): value @@ -38,9 +40,19 @@ class RootItem(FormatObject): # WARNING: Using environment variables in roots is not considered # as production safe. Some features may not work as expected, for # example USD resolver or site sync. - self.value = lowered_platform_keys[current_platform].format_map( - os.environ - ) + try: + self.value = lowered_platform_keys[current_platform].format_map( + os.environ + ) + except KeyError: + result = StringTemplate(self.value).format(os.environ.copy()) + is_are = "is" if len(result.missing_keys) == 1 else "are" + missing_keys = ", ".join(result.missing_keys) + raise RootMissingEnv( + f"Root \"{name}\" requires environment variable/s" + f" {missing_keys} which {is_are} not available." + ) + self.clean_value = self._clean_root(self.value) def __format__(self, *args, **kwargs): From 9986e7def97e620dc27b6eb69417f1a5ab1e5483 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:06:48 +0200 Subject: [PATCH 18/26] capture exception in pre launch hook --- client/ayon_core/hooks/pre_global_host_data.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/hooks/pre_global_host_data.py b/client/ayon_core/hooks/pre_global_host_data.py index 12da6f12f8..23f725901c 100644 --- a/client/ayon_core/hooks/pre_global_host_data.py +++ b/client/ayon_core/hooks/pre_global_host_data.py @@ -1,12 +1,15 @@ from ayon_api import get_project, get_folder_by_path, get_task_by_name +from ayon_core.pipeline import Anatomy +from ayon_core.pipeline.anatomy import RootMissingEnv + from ayon_applications import PreLaunchHook +from ayon_applications.exceptions import ApplicationLaunchFailed from ayon_applications.utils import ( EnvironmentPrepData, prepare_app_environments, prepare_context_environments ) -from ayon_core.pipeline import Anatomy class GlobalHostDataHook(PreLaunchHook): @@ -67,9 +70,12 @@ class GlobalHostDataHook(PreLaunchHook): self.data["project_entity"] = project_entity # Anatomy - self.data["anatomy"] = Anatomy( - project_name, project_entity=project_entity - ) + try: + self.data["anatomy"] = Anatomy( + project_name, project_entity=project_entity + ) + except RootMissingEnv as exc: + raise ApplicationLaunchFailed(str(exc)) folder_path = self.data.get("folder_path") if not folder_path: From 60848df43044af39ceecd9574439088ae3500900 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:36:38 +0200 Subject: [PATCH 19/26] added '_update' and '_pop' helper methods --- .../ayon_core/pipeline/create/structures.py | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 17bb85b720..097b5a553c 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -160,27 +160,14 @@ class AttributeValues: return self._attr_defs_by_key.get(key, default) def update(self, value): - changes = {} - for _key, _value in dict(value).items(): - if _key in self._data and self._data.get(_key) == _value: - continue - self._data[_key] = _value - changes[_key] = _value - + changes = self._update(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 - attr_def = self._attr_defs_by_key.get(key) - 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}) + value, changes = self._pop(key, default) + if changes: + self._parent.attribute_value_changed(self._key, changes) return value def reset_values(self): @@ -228,6 +215,29 @@ class AttributeValues: return serialize_attr_defs(self._attr_defs) + def _update(self, value): + changes = {} + for key, value in dict(value).items(): + if key in self._data and self._data.get(key) == value: + continue + self._data[key] = value + changes[key] = value + return changes + + def _pop(self, key, default): + 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 + attr_def = self._attr_defs_by_key.get(key) + changes = {} + if isinstance(attr_def, UnknownDef): + self._attr_defs_by_key.pop(key) + self._attr_defs.remove(attr_def) + elif has_key: + changes[key] = None + return value, changes + class CreatorAttributeValues(AttributeValues): """Creator specific attribute values of an instance.""" From 67db8e7632ec175c8155faf773884d654759eca4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:36:55 +0200 Subject: [PATCH 20/26] implemented 'set_value' on AttributeValues --- client/ayon_core/pipeline/create/structures.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 097b5a553c..17f717dd3e 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -170,6 +170,16 @@ class AttributeValues: self._parent.attribute_value_changed(self._key, changes) return value + def set_value(self, value): + pop_keys = set(value.keys()) - set(self._data.keys()) + changes = self._update(value) + for key in pop_keys: + _, key_changes = self._pop(key, None) + changes.update(key_changes) + + if changes: + self._parent.attribute_value_changed(self._key, changes) + def reset_values(self): self._data = {} From 6116a2767770b76c32cefbc6bcf39de059bbeee1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:37:43 +0200 Subject: [PATCH 21/26] implemented setitem for PublishAttributes --- client/ayon_core/pipeline/create/structures.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 17f717dd3e..f2a8a43d3f 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -290,6 +290,20 @@ class PublishAttributes: def __getitem__(self, key): return self._data[key] + def __setitem__(self, key, value): + """Set value for plugin. + + Args: + key (str): Plugin name. + value (dict[str, Any]): Value to set. + + """ + current_value = self._data.get(key) + if isinstance(current_value, PublishAttributeValues): + current_value.set_value(value) + else: + self._data[key] = value + def __contains__(self, key): return key in self._data From 0bad1078ab68ac92560572c4395fb1c7873ed1f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 2 Apr 2025 09:37:57 +0200 Subject: [PATCH 22/26] implemented delitem too --- 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 f2a8a43d3f..6b45a5c610 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -304,6 +304,9 @@ class PublishAttributes: else: self._data[key] = value + def __delitem__(self, key): + self.pop(key) + def __contains__(self, key): return key in self._data From a810be650620e8181849cfd81334ad3c64164c58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 14:59:07 +0200 Subject: [PATCH 23/26] handle popped plugin values --- client/ayon_core/pipeline/create/context.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 26b04ed3ed..6ac6685647 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -2303,10 +2303,16 @@ class CreateContext: for plugin_name, plugin_value in item_changes.pop( "publish_attributes" ).items(): + if plugin_value is None: + current_publish[plugin_name] = None + continue plugin_changes = current_publish.setdefault( plugin_name, {} ) - plugin_changes.update(plugin_value) + if plugin_changes is None: + current_publish[plugin_name] = plugin_value + else: + plugin_changes.update(plugin_value) item_values.update(item_changes) From abfb0aedee18debe9b4ec9f6e1053cdde7e1a7d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:18:49 +0200 Subject: [PATCH 24/26] changed how thumbnails are received --- client/ayon_core/pipeline/thumbnails.py | 19 +++++- .../tools/common_models/thumbnails.py | 61 +++++++++++++++++-- client/ayon_core/tools/loader/abstract.py | 12 +++- client/ayon_core/tools/loader/control.py | 11 +++- client/ayon_core/tools/loader/ui/window.py | 45 ++++++-------- 5 files changed, 109 insertions(+), 39 deletions(-) diff --git a/client/ayon_core/pipeline/thumbnails.py b/client/ayon_core/pipeline/thumbnails.py index 401d95f273..658372888f 100644 --- a/client/ayon_core/pipeline/thumbnails.py +++ b/client/ayon_core/pipeline/thumbnails.py @@ -226,11 +226,26 @@ class _CacheItems: thumbnails_cache = ThumbnailsCache() -def get_thumbnail_path(project_name, thumbnail_id): +def get_thumbnail_path( + project_name: str, + entity_type: str, + entity_id: str, + thumbnail_id: str +): """Get path to thumbnail image. + Thumbnail is cached by thumbnail id but is received using entity type and + entity id. + + Notes: + Function 'get_thumbnail_by_id' can't be used because does not work + for artists. The endpoint can't validate artist permissions. + Args: project_name (str): Project where thumbnail belongs to. + entity_type (str): Entity type "folder", "task", "version" + and "workfile". + entity_id (str): Entity id. thumbnail_id (Union[str, None]): Thumbnail id. Returns: @@ -251,7 +266,7 @@ def get_thumbnail_path(project_name, thumbnail_id): # 'get_thumbnail_by_id' did not return output of # 'ServerAPI' method. con = ayon_api.get_server_api_connection() - result = con.get_thumbnail_by_id(project_name, thumbnail_id) + result = con.get_thumbnail(project_name, entity_type, entity_id) if result is not None and result.is_valid: return _CacheItems.thumbnails_cache.store_thumbnail( diff --git a/client/ayon_core/tools/common_models/thumbnails.py b/client/ayon_core/tools/common_models/thumbnails.py index 2fa1e36e5c..5111d0be28 100644 --- a/client/ayon_core/tools/common_models/thumbnails.py +++ b/client/ayon_core/tools/common_models/thumbnails.py @@ -21,8 +21,50 @@ class ThumbnailsModel: self._folders_cache.reset() self._versions_cache.reset() - def get_thumbnail_path(self, project_name, thumbnail_id): - return self._get_thumbnail_path(project_name, thumbnail_id) + def get_thumbnail_paths( + self, + project_name, + entity_type, + entity_ids, + ): + thumbnail_paths = set() + if not project_name or not entity_type or not entity_ids: + return thumbnail_paths + + thumbnail_id_by_entity_id = {} + if entity_type == "folder": + thumbnail_id_by_entity_id = self.get_folder_thumbnail_ids( + project_name, entity_ids + ) + + elif entity_type == "version": + thumbnail_id_by_entity_id = self.get_version_thumbnail_ids( + project_name, entity_ids + ) + + if not thumbnail_id_by_entity_id: + return thumbnail_paths + + entity_ids_by_thumbnail_id = collections.defaultdict(set) + for entity_id, thumbnail_id in thumbnail_id_by_entity_id.items(): + if not thumbnail_id: + continue + entity_ids_by_thumbnail_id[thumbnail_id].add(entity_id) + + output = { + entity_id: None + for entity_id in entity_ids + } + for thumbnail_id, entity_ids in entity_ids_by_thumbnail_id.items(): + thumbnail_path = self._get_thumbnail_path( + project_name, entity_type, next(iter(entity_ids)), thumbnail_id + ) + if not thumbnail_path: + continue + for entity_id in entity_ids: + output[entity_id] = thumbnail_path + + return output def get_folder_thumbnail_ids(self, project_name, folder_ids): project_cache = self._folders_cache[project_name] @@ -56,7 +98,13 @@ class ThumbnailsModel: output[version_id] = cache.get_data() return output - def _get_thumbnail_path(self, project_name, thumbnail_id): + def _get_thumbnail_path( + self, + project_name, + entity_type, + entity_id, + thumbnail_id + ): if not thumbnail_id: return None @@ -64,7 +112,12 @@ class ThumbnailsModel: if thumbnail_id in project_cache: return project_cache[thumbnail_id] - filepath = get_thumbnail_path(project_name, thumbnail_id) + filepath = get_thumbnail_path( + project_name, + entity_type, + entity_id, + thumbnail_id + ) project_cache[thumbnail_id] = filepath return filepath diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index 26b476de1f..d0d7cd430b 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -733,7 +733,12 @@ class FrontendLoaderController(_BaseLoaderController): pass @abstractmethod - def get_thumbnail_path(self, project_name, thumbnail_id): + def get_thumbnail_paths( + self, + project_name, + entity_type, + entity_ids + ): """Get thumbnail path for thumbnail id. This method should get a path to a thumbnail based on thumbnail id. @@ -742,10 +747,11 @@ class FrontendLoaderController(_BaseLoaderController): Args: project_name (str): Project name. - thumbnail_id (str): Thumbnail id. + entity_type (str): Entity type. + entity_ids (set[str]): Entity ids. Returns: - Union[str, None]: Thumbnail path or None if not found. + dict[str, Union[str, None]]: Thumbnail path by entity id. """ pass diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index 7959a63edb..b3a80b34d4 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -259,9 +259,14 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): project_name, version_ids ) - def get_thumbnail_path(self, project_name, thumbnail_id): - return self._thumbnails_model.get_thumbnail_path( - project_name, thumbnail_id + def get_thumbnail_paths( + self, + project_name, + entity_type, + entity_ids, + ): + return self._thumbnails_model.get_thumbnail_paths( + project_name, entity_type, entity_ids ) def change_products_group(self, project_name, product_ids, group_name): diff --git a/client/ayon_core/tools/loader/ui/window.py b/client/ayon_core/tools/loader/ui/window.py index b846484c39..3d2e15c630 100644 --- a/client/ayon_core/tools/loader/ui/window.py +++ b/client/ayon_core/tools/loader/ui/window.py @@ -501,38 +501,29 @@ class LoaderWindow(QtWidgets.QWidget): self._update_thumbnails() def _update_thumbnails(self): + # TODO make this threaded and show loading animation while running project_name = self._selected_project_name - thumbnail_ids = set() + entity_type = None + entity_ids = set() if self._selected_version_ids: - thumbnail_id_by_entity_id = ( - self._controller.get_version_thumbnail_ids( - project_name, - self._selected_version_ids - ) - ) - thumbnail_ids = set(thumbnail_id_by_entity_id.values()) + entity_ids = set(self._selected_version_ids) + entity_type = "version" elif self._selected_folder_ids: - thumbnail_id_by_entity_id = ( - self._controller.get_folder_thumbnail_ids( - project_name, - self._selected_folder_ids - ) - ) - thumbnail_ids = set(thumbnail_id_by_entity_id.values()) + entity_ids = set(self._selected_folder_ids) + entity_type = "folder" - thumbnail_ids.discard(None) - - if not thumbnail_ids: - self._thumbnails_widget.set_current_thumbnails(None) - return - - thumbnail_paths = set() - for thumbnail_id in thumbnail_ids: - thumbnail_path = self._controller.get_thumbnail_path( - project_name, thumbnail_id) - thumbnail_paths.add(thumbnail_path) + thumbnail_path_by_entity_id = self._controller.get_thumbnail_paths( + project_name, entity_type, entity_ids + ) + thumbnail_paths = set(thumbnail_path_by_entity_id.values()) thumbnail_paths.discard(None) - self._thumbnails_widget.set_current_thumbnail_paths(thumbnail_paths) + + if thumbnail_paths: + self._thumbnails_widget.set_current_thumbnail_paths( + thumbnail_paths + ) + else: + self._thumbnails_widget.set_current_thumbnails(None) def _on_projects_refresh(self): self._refresh_handler.set_project_refreshed() From 76734fbbd2ee402876bfd3e3e6a1de7ec0bfcb1e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 7 Apr 2025 20:55:32 +0000 Subject: [PATCH 25/26] [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 d021a03e7e..b3c68e5390 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.6+dev" +__version__ = "1.1.7" diff --git a/package.py b/package.py index 9af45719a7..121b909486 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.6+dev" +version = "1.1.7" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 3a76b25c0d..5445048777 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.1.6+dev" +version = "1.1.7" description = "" authors = ["Ynput Team "] readme = "README.md" From abbc9ffc378dbb2c8553a3d8217a3b51e17ca699 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 7 Apr 2025 20:56:09 +0000 Subject: [PATCH 26/26] [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 b3c68e5390..962ec487a7 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.7" +__version__ = "1.1.7+dev" diff --git a/package.py b/package.py index 121b909486..fe870315cf 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.1.7" +version = "1.1.7+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 5445048777..86df6535aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.1.7" +version = "1.1.7+dev" description = "" authors = ["Ynput Team "] readme = "README.md"