From 3ddd64bbc3b12b4bbd2ac6c4d6280a91b6aed81c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:22:51 +0100 Subject: [PATCH 01/71] handle project argument --- client/ayon_core/cli.py | 75 +++++++++++++++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 6f89a6d17d..dc8ca44082 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -24,22 +24,35 @@ from ayon_core.lib.env_tools import ( ) - @click.group(invoke_without_command=True) @click.pass_context -@click.option("--use-staging", is_flag=True, - expose_value=False, help="use staging variants") -@click.option("--debug", is_flag=True, expose_value=False, - help="Enable debug") -@click.option("--verbose", expose_value=False, - help=("Change AYON log level (debug - critical or 0-50)")) -@click.option("--force", is_flag=True, hidden=True) -def main_cli(ctx, force): +@click.option( + "--use-staging", + is_flag=True, + expose_value=False, + help="use staging variants") +@click.option( + "--debug", + is_flag=True, + expose_value=False, + help="Enable debug") +@click.option( + "--project", + help="Project name") +@click.option( + "--verbose", + expose_value=False, + help="Change AYON log level (debug - critical or 0-50)") +@click.option( + "--use-dev", + is_flag=True, + expose_value=False, + help="use dev bundle") +def main_cli(ctx, *_args, **_kwargs): """AYON is main command serving as entry point to pipeline system. It wraps different commands together. """ - if ctx.invoked_subcommand is None: # Print help if headless mode is used if os.getenv("AYON_HEADLESS_MODE") == "1": @@ -60,7 +73,6 @@ def tray(force): Default action of AYON command is to launch tray widget to control basic aspects of AYON. See documentation for more information. """ - from ayon_core.tools.tray import main main(force) @@ -283,6 +295,43 @@ def _add_addons(addons_manager): ) +def _cleanup_project_args(): + rem_args = list(sys.argv[1:]) + if "--project" not in rem_args: + return + + cmd = None + current_ctx = None + parent_name = "ayon" + parent_cmd = main_cli + while hasattr(parent_cmd, "resolve_command"): + if current_ctx is None: + current_ctx = main_cli.make_context(parent_name, rem_args) + else: + current_ctx = parent_cmd.make_context( + parent_name, + rem_args, + parent=current_ctx + ) + if not rem_args: + break + cmd_name, cmd, rem_args = parent_cmd.resolve_command( + current_ctx, rem_args + ) + parent_name = cmd_name + parent_cmd = cmd + + if cmd is None: + return + + param_names = {param.name for param in cmd.params} + if "project" in param_names: + return + idx = sys.argv.index("--project") + sys.argv.pop(idx) + sys.argv.pop(idx) + + def main(*args, **kwargs): initialize_ayon_connection() python_path = os.getenv("PYTHONPATH", "") @@ -307,10 +356,14 @@ def main(*args, **kwargs): addons_manager = AddonsManager() _set_addons_environments(addons_manager) _add_addons(addons_manager) + + _cleanup_project_args() + try: main_cli( prog_name="ayon", obj={"addons_manager": addons_manager}, + args=(sys.argv[1:]), ) except Exception: # noqa exc_info = sys.exc_info() From 09fe05025c85aecbe3f7635fe8c5c53b559906d2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Mar 2025 14:23:07 +0100 Subject: [PATCH 02/71] modified settings fetching --- client/ayon_core/settings/lib.py | 68 +++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/settings/lib.py b/client/ayon_core/settings/lib.py index aa56fa8326..d251439221 100644 --- a/client/ayon_core/settings/lib.py +++ b/client/ayon_core/settings/lib.py @@ -4,6 +4,7 @@ import logging import collections import copy import time +from urllib.parse import urlencode import ayon_api @@ -35,6 +36,35 @@ class CacheItem: return time.time() > self._outdate_time +def _get_addons_settings( + studio_bundle_name, + project_bundle_name, + variant, + project_name=None, +): + """Modified version of `ayon_api.get_addons_settings` function.""" + query_values = { + key: value + for key, value in ( + ("bundle_name", studio_bundle_name), + ("project_bundle_name ", project_bundle_name), + ("variant", variant), + ("project_name", project_name), + ) + if value + } + site_id = ayon_api.get_site_id() + if site_id: + query_values["site_id"] = site_id + + response = ayon_api.get(f"settings?{urlencode(query_values)}") + response.raise_for_status() + return { + addon["name"]: addon["settings"] + for addon in response.data["addons"] + } + + class _AyonSettingsCache: use_bundles = None variant = None @@ -60,7 +90,7 @@ class _AyonSettingsCache: variant = "production" if is_dev_mode_enabled(): - variant = cls._get_bundle_name() + variant = cls._get_studio_bundle_name() elif is_staging_enabled(): variant = "staging" @@ -72,27 +102,33 @@ class _AyonSettingsCache: return _AyonSettingsCache.variant @classmethod - def _get_bundle_name(cls): + def _get_studio_bundle_name(cls): + bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME") + if bundle_name: + return bundle_name + return os.environ["AYON_BUNDLE_NAME"] + + @classmethod + def _get_project_bundle_name(cls): return os.environ["AYON_BUNDLE_NAME"] @classmethod def get_value_by_project(cls, project_name): cache_item = _AyonSettingsCache.cache_by_project_name[project_name] if cache_item.is_outdated: - if cls._use_bundles(): - value = ayon_api.get_addons_settings( - bundle_name=cls._get_bundle_name(), + cache_item.update_value( + _get_addons_settings( + studio_bundle_name=cls._get_studio_bundle_name(), + project_bundle_name=cls._get_project_bundle_name(), project_name=project_name, - variant=cls._get_variant() + variant=cls._get_variant(), ) - else: - value = ayon_api.get_addons_settings(project_name) - cache_item.update_value(value) + ) return cache_item.get_value() @classmethod def _get_addon_versions_from_bundle(cls): - expected_bundle = cls._get_bundle_name() + expected_bundle = cls._get_project_bundle_name() bundles = ayon_api.get_bundles()["bundles"] bundle = next( ( @@ -110,15 +146,9 @@ class _AyonSettingsCache: def get_addon_versions(cls): cache_item = _AyonSettingsCache.addon_versions if cache_item.is_outdated: - if cls._use_bundles(): - addons = cls._get_addon_versions_from_bundle() - else: - settings_data = ayon_api.get_addons_settings( - only_values=False, - variant=cls._get_variant() - ) - addons = settings_data["versions"] - cache_item.update_value(addons) + cache_item.update_value( + cls._get_addon_versions_from_bundle() + ) return cache_item.get_value() From 0a54f569ceb80e491e21ff1f8b80131191f0c48f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:55:26 +0100 Subject: [PATCH 03/71] fix addons discovery --- client/ayon_core/addon/base.py | 25 ++++++++++++++++++++----- client/ayon_core/cli.py | 1 + client/ayon_core/settings/lib.py | 27 ++++++++++++++++++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 72270fa585..7d02acf548 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -155,18 +155,33 @@ def load_addons(force=False): def _get_ayon_bundle_data(): + studio_bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME") + project_bundle_name = os.getenv("AYON_BUNDLE_NAME") bundles = ayon_api.get_bundles()["bundles"] - - bundle_name = os.getenv("AYON_BUNDLE_NAME") - - return next( + project_bundle = next( ( bundle for bundle in bundles - if bundle["name"] == bundle_name + if bundle["name"] == project_bundle_name ), None ) + studio_bundle = None + if studio_bundle_name and project_bundle_name != studio_bundle_name: + studio_bundle = next( + ( + bundle + for bundle in bundles + if bundle["name"] == studio_bundle_name + ), + None + ) + + if project_bundle and studio_bundle: + addons = copy.deepcopy(studio_bundle["addons"]) + addons.update(project_bundle["addons"]) + project_bundle["addons"] = addons + return project_bundle def _get_ayon_addons_information(bundle_info): diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index dc8ca44082..8f2abbaeab 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -59,6 +59,7 @@ def main_cli(ctx, *_args, **_kwargs): print(ctx.get_help()) sys.exit(0) else: + ctx.params.pop("project") ctx.forward(tray) diff --git a/client/ayon_core/settings/lib.py b/client/ayon_core/settings/lib.py index d251439221..7b4c08bc04 100644 --- a/client/ayon_core/settings/lib.py +++ b/client/ayon_core/settings/lib.py @@ -128,18 +128,35 @@ class _AyonSettingsCache: @classmethod def _get_addon_versions_from_bundle(cls): - expected_bundle = cls._get_project_bundle_name() + studio_bundle_name = cls._get_studio_bundle_name() + project_bundle_name = cls._get_project_bundle_name() bundles = ayon_api.get_bundles()["bundles"] - bundle = next( + project_bundle = next( ( bundle for bundle in bundles - if bundle["name"] == expected_bundle + if bundle["name"] == project_bundle_name ), None ) - if bundle is not None: - return bundle["addons"] + studio_bundle = None + if studio_bundle_name and project_bundle_name != studio_bundle_name: + studio_bundle = next( + ( + bundle + for bundle in bundles + if bundle["name"] == studio_bundle_name + ), + None + ) + + if studio_bundle and project_bundle: + addons = copy.deepcopy(studio_bundle["addons"]) + addons.update(project_bundle["addons"]) + project_bundle["addons"] = addons + + if project_bundle is not None: + return project_bundle["addons"] return {} @classmethod From a8baf72a7fd583fb8fb8f7701b295e00d428b907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 26 Mar 2025 17:01:54 +0100 Subject: [PATCH 04/71] automatically restart tray if is running in project bundle mode --- client/ayon_core/tools/tray/ui/tray.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index aad89b6081..f090be063e 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -240,6 +240,11 @@ class TrayManager: self.log.warning("Other tray started meanwhile. Exiting.") self.exit() + project_bundle = os.getenv("AYON_BUNDLE_NAME") + studio_bundle = os.getenv("AYON_STUDIO_BUNDLE_NAME") + if studio_bundle and project_bundle != studio_bundle: + self.restart() + def get_services_submenu(self): return self._services_submenu @@ -270,11 +275,18 @@ class TrayManager: elif is_staging_enabled(): additional_args.append("--use-staging") + if "--project" in additional_args: + idx = additional_args.index("--project") + additional_args.pop(idx) + additional_args.pop(idx) + args.extend(additional_args) envs = dict(os.environ.items()) for key in { "AYON_BUNDLE_NAME", + "AYON_STUDIO_BUNDLE_NAME", + "AYON_PROJECT_NAME", }: envs.pop(key, None) @@ -329,6 +341,7 @@ class TrayManager: return json_response({ "username": self._cached_username, "bundle": os.getenv("AYON_BUNDLE_NAME"), + "studio_bundle": os.getenv("AYON_STUDIO_BUNDLE_NAME"), "dev_mode": is_dev_mode_enabled(), "staging_mode": is_staging_enabled(), "addons": { @@ -516,6 +529,8 @@ class TrayManager: "AYON_SERVER_URL", "AYON_API_KEY", "AYON_BUNDLE_NAME", + "AYON_STUDIO_BUNDLE_NAME", + "AYON_PROJECT_NAME", }: os.environ.pop(key, None) self.restart() @@ -549,6 +564,8 @@ class TrayManager: envs = dict(os.environ.items()) for key in { "AYON_BUNDLE_NAME", + "AYON_STUDIO_BUNDLE_NAME", + "AYON_PROJECT_NAME", }: envs.pop(key, None) From c194fe2dd2c1016827dce3fcf2f162c5888a6c1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:44:10 +0100 Subject: [PATCH 05/71] set project bundle name only if is different from studio bundle name --- client/ayon_core/settings/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/settings/lib.py b/client/ayon_core/settings/lib.py index 7b4c08bc04..cd219a153b 100644 --- a/client/ayon_core/settings/lib.py +++ b/client/ayon_core/settings/lib.py @@ -47,12 +47,14 @@ def _get_addons_settings( key: value for key, value in ( ("bundle_name", studio_bundle_name), - ("project_bundle_name ", project_bundle_name), ("variant", variant), ("project_name", project_name), ) if value } + if project_bundle_name != studio_bundle_name: + query_values["project_bundle_name"] = project_bundle_name + site_id = ayon_api.get_site_id() if site_id: query_values["site_id"] = site_id From 3483a7bd0ed9a4c3d9e1f99832ddb9524db3a8b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 5 May 2025 12:02:22 +0200 Subject: [PATCH 06/71] Updates plugin families Removes "circuit" from plugin families and adds "batchdelivery" to align with current project needs. This change ensures that the collect, extract, and review processes are correctly associated with the appropriate families, streamlining publishing workflows. --- client/ayon_core/plugins/publish/collect_audio.py | 2 +- client/ayon_core/plugins/publish/extract_burnin.py | 2 +- client/ayon_core/plugins/publish/extract_review.py | 2 +- client/ayon_core/plugins/publish/extract_thumbnail.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index 57c69ef2b2..069082af37 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -39,7 +39,7 @@ class CollectAudio(pyblish.api.ContextPlugin): "blender", "houdini", "max", - "circuit", + "batchdelivery", ] audio_product_name = "audioMain" diff --git a/client/ayon_core/plugins/publish/extract_burnin.py b/client/ayon_core/plugins/publish/extract_burnin.py index 3f7c2f4cba..4b285d9990 100644 --- a/client/ayon_core/plugins/publish/extract_burnin.py +++ b/client/ayon_core/plugins/publish/extract_burnin.py @@ -55,7 +55,7 @@ class ExtractBurnin(publish.Extractor): "max", "blender", "unreal", - "circuit", + "batchdelivery", ] optional = True diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index a15886451b..7a0627d05c 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -92,7 +92,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "aftereffects", "flame", "unreal", - "circuit", + "batchdelivery", ] # Supported extensions diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 3a428c46a7..b4309a6038 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -40,7 +40,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "aftereffects", "unreal", "houdini", - "circuit", + "batchdelivery", ] enabled = False From 539be6c5270dbdfe739df9d2ccda8b59cc7b7340 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 9 May 2025 17:51:51 +0200 Subject: [PATCH 07/71] Handles OCIO shared view token The OCIO config can return a special token "" as the colorspace name for a display view. This commit implements handling for this token, replacing it with the display name if found. --- client/ayon_core/pipeline/colorspace.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 8c4f97ab1c..79aea391eb 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -1403,7 +1403,12 @@ def _get_display_view_colorspace_name(config_path, display, view): """ config = _get_ocio_config(config_path) - return config.getDisplayViewColorSpaceName(display, view) + colorspace = config.getDisplayViewColorSpaceName(display, view) + # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa + if colorspace == "": + colorspace = display + + return colorspace def _get_ocio_config_colorspaces(config_path): From a237a2441abb0e121215b77e599ee5f7910214ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 17 Jun 2025 09:25:09 +0200 Subject: [PATCH 08/71] change core support for per project bundles --- package.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.py b/package.py index 908d34ffa8..9b4a15d24e 100644 --- a/package.py +++ b/package.py @@ -6,6 +6,8 @@ client_dir = "ayon_core" plugin_for = ["ayon_server"] +project_can_override_addon_version = True + ayon_server_version = ">=1.8.4,<2.0.0" ayon_launcher_version = ">=1.0.2" ayon_required_addons = {} From 60f1fa8961154e81c47c31c2db04719f5ea28fd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:21:30 +0200 Subject: [PATCH 09/71] remove bundle names from environment variables --- client/ayon_core/tools/launcher/models/actions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index adb8d371ed..1945019fef 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -513,7 +513,12 @@ class ActionsModel: uri = payload["uri"] else: uri = data["uri"] - run_detached_ayon_launcher_process(uri) + + # Remove bundles from environment variables + env = os.environ.copy() + env.pop("AYON_BUNDLE_NAME", None) + env.pop("AYON_STUDIO_BUNDLE_NAME", None) + run_detached_ayon_launcher_process(uri, env=env) elif response_type in ("query", "navigate"): response.error_message = ( From ec3eaeb75180595fddd494cbf9b3cce97486d94e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 21 Jul 2025 19:26:47 +0200 Subject: [PATCH 10/71] added log --- client/ayon_core/tools/tray/ui/tray.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/tools/tray/ui/tray.py b/client/ayon_core/tools/tray/ui/tray.py index f090be063e..cea8d4f747 100644 --- a/client/ayon_core/tools/tray/ui/tray.py +++ b/client/ayon_core/tools/tray/ui/tray.py @@ -243,6 +243,11 @@ class TrayManager: project_bundle = os.getenv("AYON_BUNDLE_NAME") studio_bundle = os.getenv("AYON_STUDIO_BUNDLE_NAME") if studio_bundle and project_bundle != studio_bundle: + self.log.info( + f"Project bundle '{project_bundle}' is defined, but tray" + " cannot be running in project scope. Restarting tray to use" + " studio bundle." + ) self.restart() def get_services_submenu(self): From 55a7db79899be57494591e434b447bc3319245c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 23 Jul 2025 16:59:20 +0200 Subject: [PATCH 11/71] :recycle: revive linked assets/folders in template builder Adding back linked assets/folder feature that was there in template builder in OpenPype. This is now working with template type links of AYON. --- .../workfile/workfile_template_builder.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index b0fad8d2a1..276f90af80 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -313,7 +313,8 @@ class AbstractTemplateBuilder(ABC): if not folder_entity: return [] links = get_folder_links( - project_name, folder_entity["id"], link_direction="in" + project_name, + folder_entity["id"], link_types=["template"], link_direction="in" ) linked_folder_ids = { link["entityId"] @@ -1429,8 +1430,7 @@ class PlaceholderLoadMixin(object): builder_type_enum_items = [ {"label": "Current folder", "value": "context_folder"}, - # TODO implement linked folders - # {"label": "Linked folders", "value": "linked_folders"}, + {"label": "Linked folders", "value": "linked_folders"}, {"label": "All folders", "value": "all_folders"}, ] build_type_label = "Folder Builder Type" @@ -1607,10 +1607,7 @@ class PlaceholderLoadMixin(object): builder_type = placeholder.data["builder_type"] folder_ids = [] - if builder_type == "context_folder": - folder_ids = [current_folder_entity["id"]] - - elif builder_type == "all_folders": + if builder_type == "all_folders": folder_ids = { folder_entity["id"] for folder_entity in get_folders( @@ -1620,6 +1617,19 @@ class PlaceholderLoadMixin(object): ) } + elif builder_type == "context_folder": + folder_ids = [current_folder_entity["id"]] + + elif builder_type == "linked_folders": + # Get all linked folders for the current folder + if hasattr(self, "builder") and isinstance( + self.builder, AbstractTemplateBuilder): + # self.builder: AbstractTemplateBuilder + folder_ids = [ + linked_folder_entity["id"] + for linked_folder_entity in self.builder.linked_folder_entities # noqa: E501 + ] + if not folder_ids: return [] From cc9be12d22e7f5eb524d5f6eabfdb1ee9a049f46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 23 Jul 2025 17:16:11 +0200 Subject: [PATCH 12/71] :recycle: break the long line --- .../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 276f90af80..bfa192d834 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1627,7 +1627,8 @@ class PlaceholderLoadMixin(object): # self.builder: AbstractTemplateBuilder folder_ids = [ linked_folder_entity["id"] - for linked_folder_entity in self.builder.linked_folder_entities # noqa: E501 + for linked_folder_entity in ( + self.builder.linked_folder_entities) ] if not folder_ids: From 06dbaf2d635d42ad9ba82701593b37a453a5f6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 29 Jul 2025 18:01:34 +0200 Subject: [PATCH 13/71] :recycle: add link types --- .../workfile/workfile_template_builder.py | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index bfa192d834..6b82e3b04d 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -204,7 +204,9 @@ class AbstractTemplateBuilder(ABC): @property def linked_folder_entities(self): if self._linked_folder_entities is _NOT_SET: - self._linked_folder_entities = self._get_linked_folder_entities() + self._linked_folder_entities = self._get_linked_folder_entities( + link_type="template" + ) return self._linked_folder_entities @property @@ -307,14 +309,14 @@ class AbstractTemplateBuilder(ABC): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name - def _get_linked_folder_entities(self): + def _get_linked_folder_entities(self, link_type: str = "template"): project_name = self.project_name folder_entity = self.current_folder_entity if not folder_entity: return [] links = get_folder_links( project_name, - folder_entity["id"], link_types=["template"], link_direction="in" + folder_entity["id"], link_types=[link_type], link_direction="in" ) linked_folder_ids = { link["entityId"] @@ -1433,6 +1435,14 @@ class PlaceholderLoadMixin(object): {"label": "Linked folders", "value": "linked_folders"}, {"label": "All folders", "value": "all_folders"}, ] + + link_types = ayon_api.get_link_types(self.builder.project_name) + + link_types_enum_item = [ + {"label": link_type["name"], "value": link_type["linkType"]} + for link_type in link_types + + ] build_type_label = "Folder Builder Type" build_type_help = ( "Folder Builder Type\n" @@ -1461,6 +1471,17 @@ class PlaceholderLoadMixin(object): items=builder_type_enum_items, tooltip=build_type_help ), + attribute_definitions.EnumDef( + "link_type", + label="Link Type", + default="template", + items=link_types_enum_item, + tooltip=( + "Link Type\n" + "\nDefines what type of link will be used to" + " link the asset to the current folder." + ) + ), attribute_definitions.EnumDef( "product_type", label="Product type", From eaf47d8731a9dab98ff38d637984ea2d0837dc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 30 Jul 2025 18:32:09 +0200 Subject: [PATCH 14/71] :recycle: don't allow duplicate loaders --- client/ayon_core/pipeline/load/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index dc5bb0f66f..48e860e834 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -373,7 +373,7 @@ def discover_loader_plugins(project_name=None): if not project_name: project_name = get_current_project_name() project_settings = get_project_settings(project_name) - plugins = discover(LoaderPlugin) + plugins = discover(LoaderPlugin, allow_duplicates=False) hooks = discover(LoaderHookPlugin) sorted_hooks = sorted(hooks, key=lambda hook: hook.order) for plugin in plugins: From ea3b4524d405f63a698d34bac7d15fafff42831b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:31:46 +0200 Subject: [PATCH 15/71] capture 'ItemNotFoundException' error if possible --- client/ayon_core/lib/local_settings.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 91b881cf57..a582a6c1b9 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -124,6 +124,10 @@ def get_addons_resources_dir(addon_name: str, *args) -> str: return os.path.join(addons_resources_dir, addon_name, *args) +class _FakeException(Exception): + """Placeholder exception used if real exception is not available.""" + + class AYONSecureRegistry: """Store information using keyring. @@ -195,7 +199,17 @@ class AYONSecureRegistry: """ import keyring - value = keyring.get_password(self._name, name) + # Capture 'ItemNotFoundException' exception (on linux) + try: + from secretstorage.exceptions import ItemNotFoundException + except ImportError: + ItemNotFoundException = _FakeException + + try: + value = keyring.get_password(self._name, name) + except ItemNotFoundException: + value = None + if value is not None: return value From 97a3ab142c291ced73aef586bad8e6c3b62d5ab4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:46:37 +0200 Subject: [PATCH 16/71] raise dedicated exception if item is not available --- client/ayon_core/lib/local_settings.py | 41 +++++++++++++++----------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 91b881cf57..7c6459fad6 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -15,6 +15,11 @@ import ayon_api _PLACEHOLDER = object() +# TODO should use 'KeyError' or 'Exception' as base +class RegistryItemNotFound(ValueError): + """Raised when the item is not found in keyring.""" + + class _Cache: username = None @@ -187,7 +192,7 @@ class AYONSecureRegistry: value (str): Value of the item. Raises: - ValueError: If item doesn't exist and default is not defined. + RegistryItemNotFound: If item doesn't exist and default is not defined. .. _Keyring module: https://github.com/jaraco/keyring @@ -202,9 +207,8 @@ class AYONSecureRegistry: if default is not _PLACEHOLDER: return default - # NOTE Should raise `KeyError` - raise ValueError( - "Item {}:{} does not exist in keyring.".format(self._name, name) + raise RegistryItemNotFound( + f"Item {self._name}:{name} not found in keyring." ) def delete_item(self, name): @@ -277,7 +281,7 @@ class ASettingRegistry(ABC): value (str): Value of the item. Raises: - ValueError: If item doesn't exist. + RegistryItemNotFound: If the item doesn't exist. """ return self._get_item(name) @@ -388,7 +392,7 @@ class IniSettingRegistry(ASettingRegistry): str: Value of item. Raises: - ValueError: If value doesn't exist. + RegistryItemNotFound: If value doesn't exist. """ return super(IniSettingRegistry, self).get_item(name) @@ -399,8 +403,8 @@ class IniSettingRegistry(ASettingRegistry): """Get item from section of ini file. This will read ini file and try to get item value from specified - section. If that section or item doesn't exist, :exc:`ValueError` - is risen. + section. If that section or item doesn't exist, + :exc:`RegistryItemNotFound` is risen. Args: section (str): Name of ini section. @@ -410,7 +414,7 @@ class IniSettingRegistry(ASettingRegistry): str: Item value. Raises: - ValueError: If value doesn't exist. + RegistryItemNotFound: If value doesn't exist. """ config = configparser.ConfigParser() @@ -418,8 +422,9 @@ class IniSettingRegistry(ASettingRegistry): try: value = config[section][name] except KeyError: - raise ValueError( - "Registry doesn't contain value {}:{}".format(section, name)) + raise RegistryItemNotFound( + f"Registry doesn't contain value {section}:{name}" + ) return value def _get_item(self, name): @@ -435,7 +440,7 @@ class IniSettingRegistry(ASettingRegistry): name (str): Name of the item. Raises: - ValueError: If item doesn't exist. + RegistryItemNotFound: If the item doesn't exist. """ self.get_item_from_section.cache_clear() @@ -444,8 +449,9 @@ class IniSettingRegistry(ASettingRegistry): try: _ = config[section][name] except KeyError: - raise ValueError( - "Registry doesn't contain value {}:{}".format(section, name)) + raise RegistryItemNotFound( + f"Registry doesn't contain value {section}:{name}" + ) config.remove_option(section, name) # if section is empty, delete it @@ -494,8 +500,9 @@ class JSONSettingRegistry(ASettingRegistry): try: value = data["registry"][name] except KeyError: - raise ValueError( - "Registry doesn't contain value {}".format(name)) + raise RegistryItemNotFound( + f"Registry doesn't contain value {name}" + ) return value def get_item(self, name): @@ -509,7 +516,7 @@ class JSONSettingRegistry(ASettingRegistry): value of the item Raises: - ValueError: If item is not found in registry file. + RegistryItemNotFound: If the item is not found in registry file. """ return self._get_item(name) From 88b01a2797c39e9b19d5808f475c3ecb49ce885f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:54:26 +0200 Subject: [PATCH 17/71] added type-hints --- client/ayon_core/lib/local_settings.py | 101 ++++++++++--------------- 1 file changed, 38 insertions(+), 63 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 7c6459fad6..19ffffd63f 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -8,6 +8,7 @@ import warnings from datetime import datetime from abc import ABC, abstractmethod from functools import lru_cache +from typing import Optional, Any import platformdirs import ayon_api @@ -24,14 +25,14 @@ class _Cache: username = None -def _get_ayon_appdirs(*args): +def _get_ayon_appdirs(*args: str) -> str: return os.path.join( platformdirs.user_data_dir("AYON", "Ynput"), *args ) -def get_ayon_appdirs(*args): +def get_ayon_appdirs(*args: str) -> str: """Local app data directory of AYON client. Deprecated: @@ -141,7 +142,7 @@ class AYONSecureRegistry: Args: name(str): Name of registry used as identifier for data. """ - def __init__(self, name): + def __init__(self, name: str) -> None: try: import keyring @@ -159,8 +160,7 @@ class AYONSecureRegistry: # Force "AYON" prefix self._name = "/".join(("AYON", name)) - def set_item(self, name, value): - # type: (str, str) -> None + def set_item(self, name: str, value: str) -> None: """Set sensitive item into system's keyring. This uses `Keyring module`_ to save sensitive stuff into system's @@ -179,7 +179,9 @@ class AYONSecureRegistry: keyring.set_password(self._name, name, value) @lru_cache(maxsize=32) - def get_item(self, name, default=_PLACEHOLDER): + def get_item( + self, name: str, default: Any = _PLACEHOLDER + ) -> Optional[str]: """Get value of sensitive item from system's keyring. See also `Keyring module`_ @@ -211,8 +213,7 @@ class AYONSecureRegistry: f"Item {self._name}:{name} not found in keyring." ) - def delete_item(self, name): - # type: (str) -> None + def delete_item(self, name: str) -> None: """Delete value stored in system's keyring. See also `Keyring module`_ @@ -241,16 +242,13 @@ class ASettingRegistry(ABC): _name (str): Registry names. """ - - def __init__(self, name): - # type: (str) -> ASettingRegistry + def __init__(self, name: str) -> None: super(ASettingRegistry, self).__init__() self._name = name self._items = {} - def set_item(self, name, value): - # type: (str, str) -> None + def set_item(self, name: str, value: str) -> None: """Set item to settings registry. Args: @@ -261,17 +259,14 @@ class ASettingRegistry(ABC): self._set_item(name, value) @abstractmethod - def _set_item(self, name, value): - # type: (str, str) -> None - # Implement it - pass + def _set_item(self, name: str, value: str) -> None: + """Set item value to registry.""" - def __setitem__(self, name, value): + def __setitem__(self, name: str, value: str) -> None: self._items[name] = value self._set_item(name, value) - def get_item(self, name): - # type: (str) -> str + def get_item(self, name: str) -> str: """Get item from settings registry. Args: @@ -287,16 +282,13 @@ class ASettingRegistry(ABC): return self._get_item(name) @abstractmethod - def _get_item(self, name): - # type: (str) -> str - # Implement it - pass + def _get_item(self, name: str) -> str: + """Get item value from registry.""" - def __getitem__(self, name): + def __getitem__(self, name: str) -> Any: return self._get_item(name) - def delete_item(self, name): - # type: (str) -> None + def delete_item(self, name: str) -> None: """Delete item from settings registry. Args: @@ -306,12 +298,10 @@ class ASettingRegistry(ABC): self._delete_item(name) @abstractmethod - def _delete_item(self, name): - # type: (str) -> None - """Delete item from settings.""" - pass + def _delete_item(self, name: str) -> None: + """Delete item from registry.""" - def __delitem__(self, name): + def __delitem__(self, name: str) -> None: del self._items[name] self._delete_item(name) @@ -322,9 +312,7 @@ class IniSettingRegistry(ASettingRegistry): This class is using :mod:`configparser` (ini) files to store items. """ - - def __init__(self, name, path): - # type: (str, str) -> IniSettingRegistry + def __init__(self, name: str, path: str) -> None: super(IniSettingRegistry, self).__init__(name) # get registry file self._registry_file = os.path.join(path, "{}.ini".format(name)) @@ -334,8 +322,7 @@ class IniSettingRegistry(ASettingRegistry): now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") print("# {}".format(now), cfg) - def set_item_section(self, section, name, value): - # type: (str, str, str) -> None + def set_item_section(self, section: str, name: str, value: str) -> None: """Set item to specific section of ini registry. If section doesn't exists, it is created. @@ -358,12 +345,10 @@ class IniSettingRegistry(ASettingRegistry): with open(self._registry_file, mode="w") as cfg: config.write(cfg) - def _set_item(self, name, value): - # type: (str, str) -> None + def _set_item(self, name: str, value: str) -> None: self.set_item_section("MAIN", name, value) - def set_item(self, name, value): - # type: (str, str) -> None + def set_item(self, name: str, value: str) -> None: """Set item to settings ini file. This saves item to ``DEFAULT`` section of ini as each item there @@ -378,8 +363,7 @@ class IniSettingRegistry(ASettingRegistry): # we cast value to str as ini options values must be strings. super(IniSettingRegistry, self).set_item(name, str(value)) - def get_item(self, name): - # type: (str) -> str + def get_item(self, name: str) -> str: """Gets item from settings ini file. This gets settings from ``DEFAULT`` section of ini file as each item @@ -398,8 +382,7 @@ class IniSettingRegistry(ASettingRegistry): return super(IniSettingRegistry, self).get_item(name) @lru_cache(maxsize=32) - def get_item_from_section(self, section, name): - # type: (str, str) -> str + def get_item_from_section(self, section: str, name: str) -> str: """Get item from section of ini file. This will read ini file and try to get item value from specified @@ -427,12 +410,10 @@ class IniSettingRegistry(ASettingRegistry): ) return value - def _get_item(self, name): - # type: (str) -> str + def _get_item(self, name: str) -> str: return self.get_item_from_section("MAIN", name) - def delete_item_from_section(self, section, name): - # type: (str, str) -> None + def delete_item_from_section(self, section: str, name: str) -> None: """Delete item from section in ini file. Args: @@ -469,8 +450,7 @@ class IniSettingRegistry(ASettingRegistry): class JSONSettingRegistry(ASettingRegistry): """Class using json file as storage.""" - def __init__(self, name, path): - # type: (str, str) -> JSONSettingRegistry + def __init__(self, name: str, path: str) -> None: super(JSONSettingRegistry, self).__init__(name) #: str: name of registry file self._registry_file = os.path.join(path, "{}.json".format(name)) @@ -487,8 +467,7 @@ class JSONSettingRegistry(ASettingRegistry): json.dump(header, cfg, indent=4) @lru_cache(maxsize=32) - def _get_item(self, name): - # type: (str) -> object + def _get_item(self, name: str) -> Any: """Get item value from registry json. Note: @@ -505,8 +484,7 @@ class JSONSettingRegistry(ASettingRegistry): ) return value - def get_item(self, name): - # type: (str) -> object + def get_item(self, name: str) -> Any: """Get item value from registry json. Args: @@ -521,8 +499,7 @@ class JSONSettingRegistry(ASettingRegistry): """ return self._get_item(name) - def _set_item(self, name, value): - # type: (str, object) -> None + def _set_item(self, name: str, value: Any) -> None: """Set item value to registry json. Note: @@ -536,8 +513,7 @@ class JSONSettingRegistry(ASettingRegistry): cfg.seek(0) json.dump(data, cfg, indent=4) - def set_item(self, name, value): - # type: (str, object) -> None + def set_item(self, name: str, value: Any) -> None: """Set item and its value into json registry file. Args: @@ -547,8 +523,7 @@ class JSONSettingRegistry(ASettingRegistry): """ self._set_item(name, value) - def _delete_item(self, name): - # type: (str) -> None + def _delete_item(self, name: str) -> None: self._get_item.cache_clear() with open(self._registry_file, "r+") as cfg: data = json.load(cfg) @@ -563,9 +538,9 @@ class AYONSettingsRegistry(JSONSettingRegistry): Args: name (Optional[str]): Name of the registry. - """ - def __init__(self, name=None): + """ + def __init__(self, name: Optional[str] = None) -> None: if not name: name = "AYON_settings" path = get_launcher_storage_dir() From d431956963c55bb60405142649efd636011b89ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:55:34 +0200 Subject: [PATCH 18/71] simplified super calls --- client/ayon_core/lib/local_settings.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 19ffffd63f..26db587835 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -243,7 +243,7 @@ class ASettingRegistry(ABC): """ def __init__(self, name: str) -> None: - super(ASettingRegistry, self).__init__() + super().__init__() self._name = name self._items = {} @@ -313,7 +313,7 @@ class IniSettingRegistry(ASettingRegistry): """ def __init__(self, name: str, path: str) -> None: - super(IniSettingRegistry, self).__init__(name) + super().__init__(name) # get registry file self._registry_file = os.path.join(path, "{}.ini".format(name)) if not os.path.exists(self._registry_file): @@ -361,7 +361,7 @@ class IniSettingRegistry(ASettingRegistry): """ # this does the some, overridden just for different docstring. # we cast value to str as ini options values must be strings. - super(IniSettingRegistry, self).set_item(name, str(value)) + super().set_item(name, str(value)) def get_item(self, name: str) -> str: """Gets item from settings ini file. @@ -379,7 +379,7 @@ class IniSettingRegistry(ASettingRegistry): RegistryItemNotFound: If value doesn't exist. """ - return super(IniSettingRegistry, self).get_item(name) + return super().get_item(name) @lru_cache(maxsize=32) def get_item_from_section(self, section: str, name: str) -> str: @@ -451,7 +451,7 @@ class JSONSettingRegistry(ASettingRegistry): """Class using json file as storage.""" def __init__(self, name: str, path: str) -> None: - super(JSONSettingRegistry, self).__init__(name) + super().__init__(name) #: str: name of registry file self._registry_file = os.path.join(path, "{}.json".format(name)) now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") @@ -544,7 +544,7 @@ class AYONSettingsRegistry(JSONSettingRegistry): if not name: name = "AYON_settings" path = get_launcher_storage_dir() - super(AYONSettingsRegistry, self).__init__(name, path) + super().__init__(name, path) def get_local_site_id(): From 08c242edefe4b11046a589fca10a7e8e7f969177 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:55:59 +0200 Subject: [PATCH 19/71] use f-strings --- client/ayon_core/lib/local_settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 26db587835..36abeb4283 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -158,7 +158,7 @@ class AYONSecureRegistry: keyring.set_keyring(Windows.WinVaultKeyring()) # Force "AYON" prefix - self._name = "/".join(("AYON", name)) + self._name = f"AYON/{name}" def set_item(self, name: str, value: str) -> None: """Set sensitive item into system's keyring. @@ -315,12 +315,12 @@ class IniSettingRegistry(ASettingRegistry): def __init__(self, name: str, path: str) -> None: super().__init__(name) # get registry file - self._registry_file = os.path.join(path, "{}.ini".format(name)) + self._registry_file = os.path.join(path, f"{name}.ini") if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: print("# Settings registry", cfg) now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") - print("# {}".format(now), cfg) + print(f"# {now}", cfg) def set_item_section(self, section: str, name: str, value: str) -> None: """Set item to specific section of ini registry. @@ -452,8 +452,7 @@ class JSONSettingRegistry(ASettingRegistry): def __init__(self, name: str, path: str) -> None: super().__init__(name) - #: str: name of registry file - self._registry_file = os.path.join(path, "{}.json".format(name)) + self._registry_file = os.path.join(path, f"{name}.json") now = datetime.now().strftime("%d/%m/%Y %H:%M:%S") header = { "__metadata__": {"generated": now}, From d1fce584fa577d209feff5c91867eda12399acec Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:56:25 +0200 Subject: [PATCH 20/71] remove unncessary variable --- client/ayon_core/lib/local_settings.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 36abeb4283..a52539a4dd 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -238,15 +238,11 @@ class ASettingRegistry(ABC): mechanism for storing common items must be implemented in abstract methods. - Attributes: - _name (str): Registry names. - """ def __init__(self, name: str) -> None: super().__init__() self._name = name - self._items = {} def set_item(self, name: str, value: str) -> None: """Set item to settings registry. @@ -263,7 +259,6 @@ class ASettingRegistry(ABC): """Set item value to registry.""" def __setitem__(self, name: str, value: str) -> None: - self._items[name] = value self._set_item(name, value) def get_item(self, name: str) -> str: @@ -302,7 +297,6 @@ class ASettingRegistry(ABC): """Delete item from registry.""" def __delitem__(self, name: str) -> None: - del self._items[name] self._delete_item(name) From d88a8678729fc2e51c9e49e7f798499e5e74cdcd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:56:39 +0200 Subject: [PATCH 21/71] reset cache on set item --- client/ayon_core/lib/local_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index a52539a4dd..162e17fd94 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -177,6 +177,7 @@ class AYONSecureRegistry: import keyring keyring.set_password(self._name, name, value) + self.get_item.cache_clear() @lru_cache(maxsize=32) def get_item( From 41228915eca2c0b29fdc7d7c5eb208ecda0fd568 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:58:08 +0200 Subject: [PATCH 22/71] more explicit dir creation --- client/ayon_core/lib/local_settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 162e17fd94..98eec3af4f 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -454,8 +454,10 @@ class JSONSettingRegistry(ASettingRegistry): "registry": {} } - if not os.path.exists(os.path.dirname(self._registry_file)): - os.makedirs(os.path.dirname(self._registry_file), exist_ok=True) + # Use 'os.path.dirname' in case someone uses slashes in 'name' + dirpath = os.path.dirname(self._registry_file) + if not os.path.exists(dirpath): + os.makedirs(dirpath, exist_ok=True) if not os.path.exists(self._registry_file): with open(self._registry_file, mode="w") as cfg: json.dump(header, cfg, indent=4) From 2013eea5c4cc52942f79c371032f7c9b6126870a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 10:58:21 +0200 Subject: [PATCH 23/71] formatting change --- client/ayon_core/lib/local_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 98eec3af4f..b06b890992 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -141,6 +141,7 @@ class AYONSecureRegistry: Args: name(str): Name of registry used as identifier for data. + """ def __init__(self, name: str) -> None: try: From 1a46f2c027b60e03ac35a25d231bf034042bcb1c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:00:51 +0200 Subject: [PATCH 24/71] remove unnecessary super call --- client/ayon_core/lib/local_settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index b06b890992..79e0e24307 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -242,8 +242,6 @@ class ASettingRegistry(ABC): """ def __init__(self, name: str) -> None: - super().__init__() - self._name = name def set_item(self, name: str, value: str) -> None: From b403db76e6626b450dad91d489e20bc997284746 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:01:52 +0200 Subject: [PATCH 25/71] better order of methods --- client/ayon_core/lib/local_settings.py | 46 ++++++++++++++------------ 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 79e0e24307..4b85a76b2d 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -244,23 +244,31 @@ class ASettingRegistry(ABC): def __init__(self, name: str) -> None: self._name = name - def set_item(self, name: str, value: str) -> None: - """Set item to settings registry. - - Args: - name (str): Name of the item. - value (str): Value of the item. - - """ - self._set_item(name, value) + @abstractmethod + def _get_item(self, name: str) -> Any: + """Get item value from registry.""" @abstractmethod def _set_item(self, name: str, value: str) -> None: """Set item value to registry.""" + @abstractmethod + def _delete_item(self, name: str) -> None: + """Delete item from registry.""" + + def __getitem__(self, name: str) -> Any: + return self._get_item(name) + def __setitem__(self, name: str, value: str) -> None: self._set_item(name, value) + def __delitem__(self, name: str) -> None: + self._delete_item(name) + + @property + def name(self) -> str: + return self._name + def get_item(self, name: str) -> str: """Get item from settings registry. @@ -276,12 +284,15 @@ class ASettingRegistry(ABC): """ return self._get_item(name) - @abstractmethod - def _get_item(self, name: str) -> str: - """Get item value from registry.""" + def set_item(self, name: str, value: str) -> None: + """Set item to settings registry. - def __getitem__(self, name: str) -> Any: - return self._get_item(name) + Args: + name (str): Name of the item. + value (str): Value of the item. + + """ + self._set_item(name, value) def delete_item(self, name: str) -> None: """Delete item from settings registry. @@ -292,13 +303,6 @@ class ASettingRegistry(ABC): """ self._delete_item(name) - @abstractmethod - def _delete_item(self, name: str) -> None: - """Delete item from registry.""" - - def __delitem__(self, name: str) -> None: - self._delete_item(name) - class IniSettingRegistry(ASettingRegistry): """Class using :mod:`configparser`. From d4092c8e314eb5e02cf004e07ac8a5f69f39a36c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:02:12 +0200 Subject: [PATCH 26/71] deprecated not passed name in 'AYONSettingsRegistry' --- client/ayon_core/lib/local_settings.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 4b85a76b2d..8511c8d15e 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -536,12 +536,21 @@ class AYONSettingsRegistry(JSONSettingRegistry): """Class handling AYON general settings registry. Args: - name (Optional[str]): Name of the registry. + name (Optional[str]): Name of the registry. Using 'None' or not + passing name is deprecated. """ def __init__(self, name: Optional[str] = None) -> None: if not name: name = "AYON_settings" + warnings.warn( + ( + "Used 'AYONSettingsRegistry' without 'name' argument." + " The argument will be required in future versions." + ), + DeprecationWarning, + stacklevel=2, + ) path = get_launcher_storage_dir() super().__init__(name, path) From 473cf8b0c13f19d4763d2d9bea4f0b376ca85f77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:19:39 +0200 Subject: [PATCH 27/71] grammar fixes --- client/ayon_core/lib/local_settings.py | 37 +++++++++++++------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 8511c8d15e..19381b18e0 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -18,7 +18,7 @@ _PLACEHOLDER = object() # TODO should use 'KeyError' or 'Exception' as base class RegistryItemNotFound(ValueError): - """Raised when the item is not found in keyring.""" + """Raised when the item is not found in the keyring.""" class _Cache: @@ -37,10 +37,10 @@ def get_ayon_appdirs(*args: str) -> str: Deprecated: Use 'get_launcher_local_dir' or 'get_launcher_storage_dir' based on - use-case. Deprecation added 24/08/09 (0.4.4-dev.1). + a use-case. Deprecation added 24/08/09 (0.4.4-dev.1). Args: - *args (Iterable[str]): Subdirectories/files in local app data dir. + *args (Iterable[str]): Subdirectories/files in the local app data dir. Returns: str: Path to directory/file in local app data dir. @@ -58,7 +58,7 @@ def get_ayon_appdirs(*args: str) -> str: def get_launcher_storage_dir(*subdirs: str) -> str: - """Get storage directory for launcher. + """Get a storage directory for launcher. Storage directory is used for storing shims, addons, dependencies, etc. @@ -83,14 +83,14 @@ def get_launcher_storage_dir(*subdirs: str) -> str: def get_launcher_local_dir(*subdirs: str) -> str: - """Get local directory for launcher. + """Get a local directory for launcher. - Local directory is used for storing machine or user specific data. + Local directory is used for storing machine or user-specific data. - The location is user specific. + The location is user-specific. Note: - This function should be called at least once on bootstrap. + This function should be called at least once on the bootstrap. Args: *subdirs (str): Subdirectories relative to local dir. @@ -107,7 +107,7 @@ def get_launcher_local_dir(*subdirs: str) -> str: def get_addons_resources_dir(addon_name: str, *args) -> str: - """Get directory for storing resources for addons. + """Get a directory for storing resources for addons. Some addons might need to store ad-hoc resources that are not part of addon client package (e.g. because of size). Studio might define @@ -117,7 +117,7 @@ def get_addons_resources_dir(addon_name: str, *args) -> str: Args: addon_name (str): Addon name. - *args (str): Subfolders in resources directory. + *args (str): Subfolders in the resources directory. Returns: str: Path to resources directory. @@ -140,7 +140,7 @@ class AYONSecureRegistry: identify which data were created by AYON. Args: - name(str): Name of registry used as identifier for data. + name(str): Name of registry used as the identifier for data. """ def __init__(self, name: str) -> None: @@ -162,9 +162,9 @@ class AYONSecureRegistry: self._name = f"AYON/{name}" def set_item(self, name: str, value: str) -> None: - """Set sensitive item into system's keyring. + """Set sensitive item into the system's keyring. - This uses `Keyring module`_ to save sensitive stuff into system's + This uses `Keyring module`_ to save sensitive stuff into the system's keyring. Args: @@ -184,19 +184,20 @@ class AYONSecureRegistry: def get_item( self, name: str, default: Any = _PLACEHOLDER ) -> Optional[str]: - """Get value of sensitive item from system's keyring. + """Get value of sensitive item from the system's keyring. See also `Keyring module`_ Args: name (str): Name of the item. - default (Any): Default value if item is not available. + default (Any): Default value if the item is not available. Returns: value (str): Value of the item. Raises: - RegistryItemNotFound: If item doesn't exist and default is not defined. + RegistryItemNotFound: If the item doesn't exist and default + is not defined. .. _Keyring module: https://github.com/jaraco/keyring @@ -216,7 +217,7 @@ class AYONSecureRegistry: ) def delete_item(self, name: str) -> None: - """Delete value stored in system's keyring. + """Delete value stored in the system's keyring. See also `Keyring module`_ @@ -446,7 +447,7 @@ class IniSettingRegistry(ASettingRegistry): class JSONSettingRegistry(ASettingRegistry): - """Class using json file as storage.""" + """Class using a json file as storage.""" def __init__(self, name: str, path: str) -> None: super().__init__(name) From 3c867c517c4773b75aae84d049a1edf5e8323512 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:20:08 +0200 Subject: [PATCH 28/71] change value of json registry to 'str' --- client/ayon_core/lib/local_settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 19381b18e0..09855c6075 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -467,8 +467,8 @@ class JSONSettingRegistry(ASettingRegistry): json.dump(header, cfg, indent=4) @lru_cache(maxsize=32) - def _get_item(self, name: str) -> Any: - """Get item value from registry json. + def _get_item(self, name: str) -> str: + """Get item value from the registry. Note: See :meth:`ayon_core.lib.JSONSettingRegistry.get_item` @@ -499,8 +499,8 @@ class JSONSettingRegistry(ASettingRegistry): """ return self._get_item(name) - def _set_item(self, name: str, value: Any) -> None: - """Set item value to registry json. + def _set_item(self, name: str, value: str) -> None: + """Set item value to the registry. Note: See :meth:`ayon_core.lib.JSONSettingRegistry.set_item` From 6433ada42c78a586899c9329b91838a5ade936e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:20:50 +0200 Subject: [PATCH 29/71] remove unnecessary overriden methods --- client/ayon_core/lib/local_settings.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 09855c6075..7982a2797e 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -484,21 +484,6 @@ class JSONSettingRegistry(ASettingRegistry): ) return value - def get_item(self, name: str) -> Any: - """Get item value from registry json. - - Args: - name (str): Name of the item. - - Returns: - value of the item - - Raises: - RegistryItemNotFound: If the item is not found in registry file. - - """ - return self._get_item(name) - def _set_item(self, name: str, value: str) -> None: """Set item value to the registry. @@ -513,18 +498,7 @@ class JSONSettingRegistry(ASettingRegistry): cfg.seek(0) json.dump(data, cfg, indent=4) - def set_item(self, name: str, value: Any) -> None: - """Set item and its value into json registry file. - - Args: - name (str): name of the item. - value (Any): value of the item. - - """ - self._set_item(name, value) - def _delete_item(self, name: str) -> None: - self._get_item.cache_clear() with open(self._registry_file, "r+") as cfg: data = json.load(cfg) del data["registry"][name] From 83b109be28602958e1fa0b666fc3e10188ad7cae Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:21:11 +0200 Subject: [PATCH 30/71] fix cache again --- client/ayon_core/lib/local_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 7982a2797e..4cfe059e2a 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -497,6 +497,7 @@ class JSONSettingRegistry(ASettingRegistry): cfg.truncate(0) cfg.seek(0) json.dump(data, cfg, indent=4) + self._get_item.cache_clear() def _delete_item(self, name: str) -> None: with open(self._registry_file, "r+") as cfg: @@ -505,6 +506,7 @@ class JSONSettingRegistry(ASettingRegistry): cfg.truncate(0) cfg.seek(0) json.dump(data, cfg, indent=4) + self._get_item.cache_clear() class AYONSettingsRegistry(JSONSettingRegistry): From 1017becebd118f5cdd3bd021ed7fbe5891bf954e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 11:21:26 +0200 Subject: [PATCH 31/71] changed abstract class docstring --- client/ayon_core/lib/local_settings.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 4cfe059e2a..85ece54d6f 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -235,11 +235,7 @@ class AYONSecureRegistry: class ASettingRegistry(ABC): - """Abstract class defining structure of **SettingRegistry** class. - - It is implementing methods to store secure items into keyring, otherwise - mechanism for storing common items must be implemented in abstract - methods. + """Abstract class to defining structure of registry class. """ def __init__(self, name: str) -> None: From 47af183d04f82ccc5d27af20143ee03ddf8eeb49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:12:00 +0200 Subject: [PATCH 32/71] check for availability that don't live in workdir --- client/ayon_core/host/interfaces/workfiles.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/host/interfaces/workfiles.py b/client/ayon_core/host/interfaces/workfiles.py index b6c33337e9..693aac5fe5 100644 --- a/client/ayon_core/host/interfaces/workfiles.py +++ b/client/ayon_core/host/interfaces/workfiles.py @@ -1155,12 +1155,15 @@ class IWorkfileHost: comment = parsed_data.comment filepath = list_workfiles_context.anatomy.fill_root(rootless_path) + available = False + if filepath != rootless_path: + available = os.path.exists(filepath) items.append(WorkfileInfo.new( filepath, rootless_path, version=version, comment=comment, - available=False, + available=available, workfile_entity=workfile_entity, )) From 4f296e0ed78138cef63791605c40e1a6b82e7ebf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:41:33 +0200 Subject: [PATCH 33/71] simplified --- client/ayon_core/host/interfaces/workfiles.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/host/interfaces/workfiles.py b/client/ayon_core/host/interfaces/workfiles.py index 693aac5fe5..14e60bda20 100644 --- a/client/ayon_core/host/interfaces/workfiles.py +++ b/client/ayon_core/host/interfaces/workfiles.py @@ -1155,9 +1155,7 @@ class IWorkfileHost: comment = parsed_data.comment filepath = list_workfiles_context.anatomy.fill_root(rootless_path) - available = False - if filepath != rootless_path: - available = os.path.exists(filepath) + available = os.path.exists(filepath) items.append(WorkfileInfo.new( filepath, rootless_path, From a85cf5d2e907c13dc19d1818231fc28ba0829000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 31 Jul 2025 15:41:47 +0200 Subject: [PATCH 34/71] :recycle: handle more link types --- .../workfile/workfile_template_builder.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 6b82e3b04d..7920abb23f 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -201,14 +201,6 @@ class AbstractTemplateBuilder(ABC): ) return self._current_folder_entity - @property - def linked_folder_entities(self): - if self._linked_folder_entities is _NOT_SET: - self._linked_folder_entities = self._get_linked_folder_entities( - link_type="template" - ) - return self._linked_folder_entities - @property def current_task_entity(self): if self._current_task_entity is _NOT_SET: @@ -309,7 +301,7 @@ class AbstractTemplateBuilder(ABC): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name - def _get_linked_folder_entities(self, link_type: str = "template"): + def get_linked_folder_entities(self, link_type: str = "template"): project_name = self.project_name folder_entity = self.current_folder_entity if not folder_entity: @@ -1642,6 +1634,8 @@ class PlaceholderLoadMixin(object): folder_ids = [current_folder_entity["id"]] elif builder_type == "linked_folders": + # link type from placeholder data or default to "template" + link_type = placeholder.data.get("link_type", "template") # Get all linked folders for the current folder if hasattr(self, "builder") and isinstance( self.builder, AbstractTemplateBuilder): @@ -1649,7 +1643,8 @@ class PlaceholderLoadMixin(object): folder_ids = [ linked_folder_entity["id"] for linked_folder_entity in ( - self.builder.linked_folder_entities) + self.builder.get_linked_folder_entities( + link_type=link_type)) ] if not folder_ids: From b247762806f3e9f4dfa38afdb812d3da461d34d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:44:02 +0200 Subject: [PATCH 35/71] make 'get_plugin_paths' optional --- client/ayon_core/addon/interfaces.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index 232c056fb4..b0f2d25c08 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -48,14 +48,23 @@ class IPluginPaths(AYONInterface): } """ - @abstractmethod def get_plugin_paths(self) -> dict[str, list[str]]: """Return plugin paths for addon. + This method was abstract (required) in the past, so raise the required + 'core' addon version when 'get_plugin_paths' is removed from + addon. + + Deprecated: + Please implement specific methods 'get_create_plugin_paths', + 'get_load_plugin_paths', 'get_inventory_action_paths' and + 'get_publish_plugin_paths' to return plugin paths. + Returns: dict[str, list[str]]: Plugin paths for addon. """ + return {} def _get_plugin_paths_by_type( self, plugin_type: str) -> list[str]: From 67f039bf5dc102b3da3c97e1010efe0639745a84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:44:36 +0200 Subject: [PATCH 36/71] warn about using deprecated method --- client/ayon_core/addon/interfaces.py | 29 ++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index b0f2d25c08..010a5aaca1 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -1,6 +1,7 @@ """Addon interfaces for AYON.""" from __future__ import annotations +import warnings from abc import ABCMeta, abstractmethod from typing import TYPE_CHECKING, Callable, Optional, Type @@ -39,14 +40,7 @@ class AYONInterface(metaclass=_AYONInterfaceMeta): class IPluginPaths(AYONInterface): - """Addon has plugin paths to return. - - Expected result is dictionary with keys "publish", "create", "load", - "actions" or "inventory" and values as list or string. - { - "publish": ["path/to/publish_plugins"] - } - """ + """Addon wants to register plugin paths.""" def get_plugin_paths(self) -> dict[str, list[str]]: """Return plugin paths for addon. @@ -87,6 +81,25 @@ class IPluginPaths(AYONInterface): if not isinstance(paths, (list, tuple, set)): paths = [paths] + + new_function_name = "get_launcher_action_paths" + if plugin_type == "create": + new_function_name = "get_create_plugin_paths" + elif plugin_type == "load": + new_function_name = "get_load_plugin_paths" + elif plugin_type == "publish": + new_function_name = "get_publish_plugin_paths" + elif plugin_type == "inventory": + new_function_name = "get_inventory_action_paths" + + warnings.warn( + f"Addon '{self.name}' returns '{plugin_type}' paths using" + " 'get_plugin_paths' method. Please implement" + f" '{new_function_name}' instead.", + DeprecationWarning, + stacklevel=2 + + ) return paths def get_launcher_action_paths(self) -> list[str]: From 487b5dda98a3ff19084d0c6143de7da0708209b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:44:45 +0200 Subject: [PATCH 37/71] small formatting change --- client/ayon_core/addon/interfaces.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index 010a5aaca1..9f2a14a264 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -61,7 +61,8 @@ class IPluginPaths(AYONInterface): return {} def _get_plugin_paths_by_type( - self, plugin_type: str) -> list[str]: + self, plugin_type: str + ) -> list[str]: """Get plugin paths by type. Args: From ba4412577bafaf5e759e39d942133d03a7a0392b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:45:12 +0200 Subject: [PATCH 38/71] mark 'collect_plugin_paths' as deprecated --- client/ayon_core/addon/base.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 72270fa585..80e1ceaa1e 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -8,6 +8,7 @@ import inspect import logging import threading import collections +import warnings from uuid import uuid4 from abc import ABC, abstractmethod from typing import Optional @@ -815,10 +816,26 @@ class AddonsManager: Unknown keys are logged out. + Deprecated: + Use targeted methods 'collect_launcher_action_paths', + 'collect_create_plugin_paths', 'collect_load_plugin_paths', + 'collect_publish_plugin_paths' and + 'collect_inventory_action_paths' to collect plugin paths. + Returns: dict: Output is dictionary with keys "publish", "create", "load", "actions" and "inventory" each containing list of paths. + """ + warnings.warn( + "Used deprecated method 'collect_plugin_paths'. Please use" + " targeted methods 'collect_launcher_action_paths'," + " 'collect_create_plugin_paths', 'collect_load_plugin_paths'" + " 'collect_publish_plugin_paths' and" + " 'collect_inventory_action_paths'", + DeprecationWarning, + stacklevel=2 + ) # Output structure output = { "publish": [], From 5e8dece22e2fbae722f83f45d74d30cad04708cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:46:03 +0200 Subject: [PATCH 39/71] warn about having string as output from plugin getter method --- client/ayon_core/addon/base.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 80e1ceaa1e..57968b0e09 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -891,24 +891,28 @@ class AddonsManager: if not isinstance(addon, IPluginPaths): continue + paths = None method = getattr(addon, method_name) try: paths = method(*args, **kwargs) except Exception: self.log.warning( - ( - "Failed to get plugin paths from addon" - " '{}' using '{}'." - ).format(addon.__class__.__name__, method_name), + "Failed to get plugin paths from addon" + f" '{addon.name}' using '{method_name}'.", exc_info=True ) + + if not paths: continue - if paths: - # Convert to list if value is not list - if not isinstance(paths, (list, tuple, set)): - paths = [paths] - output.extend(paths) + if isinstance(paths, str): + paths = [paths] + self.log.warning( + f"Addon '{addon.name}' returned invalid output type" + f" from '{method_name}'." + f" Got 'str' expected 'list[str]'." + ) + output.extend(paths) return output def collect_launcher_action_paths(self): From 2ee875b90b98cc501f9887867ab090b053b03038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 31 Jul 2025 17:17:44 +0200 Subject: [PATCH 40/71] :recycle: remove defaults --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 7920abb23f..7b20747768 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -301,7 +301,7 @@ class AbstractTemplateBuilder(ABC): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name - def get_linked_folder_entities(self, link_type: str = "template"): + def get_linked_folder_entities(self, link_type: str): project_name = self.project_name folder_entity = self.current_folder_entity if not folder_entity: @@ -1466,7 +1466,6 @@ class PlaceholderLoadMixin(object): attribute_definitions.EnumDef( "link_type", label="Link Type", - default="template", items=link_types_enum_item, tooltip=( "Link Type\n" From 5fa11b24e4bb0b165b67cd779d0e8c25ed8a2984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 31 Jul 2025 17:21:21 +0200 Subject: [PATCH 41/71] :recycle: limit link types to folder <-> folder --- .../workfile/workfile_template_builder.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 7b20747768..6a36fd12e4 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -1430,11 +1430,21 @@ class PlaceholderLoadMixin(object): link_types = ayon_api.get_link_types(self.builder.project_name) - link_types_enum_item = [ + # Filter link types for folder to folder links + link_types_enum_items = [ {"label": link_type["name"], "value": link_type["linkType"]} for link_type in link_types - + if ( + link_type["inputType"] == "folder" + and link_type["outputType"] == "folder" + ) ] + + if not link_types_enum_items: + link_types_enum_items.append( + {"label": "", "value": None} + ) + build_type_label = "Folder Builder Type" build_type_help = ( "Folder Builder Type\n" @@ -1466,7 +1476,7 @@ class PlaceholderLoadMixin(object): attribute_definitions.EnumDef( "link_type", label="Link Type", - items=link_types_enum_item, + items=link_types_enum_items, tooltip=( "Link Type\n" "\nDefines what type of link will be used to" From 8e2f33d483791c606cdbc4230e04be2ad88f6889 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 11:23:44 +0200 Subject: [PATCH 42/71] use filepath instead of rootless path for workfile entity mapping --- client/ayon_core/host/interfaces/workfiles.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/host/interfaces/workfiles.py b/client/ayon_core/host/interfaces/workfiles.py index 14e60bda20..b519751ba2 100644 --- a/client/ayon_core/host/interfaces/workfiles.py +++ b/client/ayon_core/host/interfaces/workfiles.py @@ -1072,10 +1072,13 @@ class IWorkfileHost: prepared_data=prepared_data, ) - workfile_entities_by_path = { - workfile_entity["path"]: workfile_entity - for workfile_entity in list_workfiles_context.workfile_entities - } + workfile_entities_by_path = {} + for workfile_entity in list_workfiles_context.workfile_entities: + rootless_path = workfile_entity["path"] + path = os.path.normpath( + list_workfiles_context.anatomy.fill_root(rootless_path) + ) + workfile_entities_by_path[path] = workfile_entity workdir_data = get_template_data( list_workfiles_context.project_entity, @@ -1114,10 +1117,10 @@ class IWorkfileHost: rootless_path = f"{rootless_workdir}/{filename}" workfile_entity = workfile_entities_by_path.pop( - rootless_path, None + filepath, None ) version = comment = None - if workfile_entity: + if workfile_entity is not None: _data = workfile_entity["data"] version = _data.get("version") comment = _data.get("comment") @@ -1137,7 +1140,7 @@ class IWorkfileHost: ) items.append(item) - for workfile_entity in workfile_entities_by_path.values(): + for filepath, workfile_entity in workfile_entities_by_path.items(): # Workfile entity is not in the filesystem # but it is in the database rootless_path = workfile_entity["path"] @@ -1154,7 +1157,6 @@ class IWorkfileHost: version = parsed_data.version comment = parsed_data.comment - filepath = list_workfiles_context.anatomy.fill_root(rootless_path) available = os.path.exists(filepath) items.append(WorkfileInfo.new( filepath, From e7ea930d557d5e8264beb09a94b39044178fee33 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Aug 2025 10:23:24 +0000 Subject: [PATCH 43/71] [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 7f55a17a01..c16b31f2fc 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.5.0+dev" +__version__ = "1.5.1" diff --git a/package.py b/package.py index 807e4e4b35..9c131794d7 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.0+dev" +version = "1.5.1" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e7977a5579..686cc1e3f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.0+dev" +version = "1.5.1" description = "" authors = ["Ynput Team "] readme = "README.md" From 302619176bbc651642dbe847d98eda76f543bd29 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Aug 2025 10:24:02 +0000 Subject: [PATCH 44/71] [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 c16b31f2fc..784105572b 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.5.1" +__version__ = "1.5.1+dev" diff --git a/package.py b/package.py index 9c131794d7..a0d7b26703 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.1" +version = "1.5.1+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 686cc1e3f8..b544afa346 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.1" +version = "1.5.1+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 745a394cdddd688a054870c2ae08559683850d82 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 10:24:54 +0000 Subject: [PATCH 45/71] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 9202190f8b..364d1709e0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.5.1 - 1.5.0 - 1.4.1 - 1.4.0 From 9f456f7cb8b3afa3c32ba605ca7ed22276b47c6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:37:21 +0200 Subject: [PATCH 46/71] added safe importing of otio --- .../publish/collect_otio_frame_ranges.py | 17 +++++++++++------ .../publish/extract_otio_audio_tracks.py | 2 +- .../plugins/publish/extract_otio_review.py | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) 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 0a4efc2172..d68970d428 100644 --- a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py +++ b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py @@ -8,13 +8,7 @@ This module contains a unified plugin that handles: from pprint import pformat -import opentimelineio as otio import pyblish.api -from ayon_core.pipeline.editorial import ( - get_media_range_with_retimes, - otio_range_to_frame_range, - otio_range_with_handles, -) def validate_otio_clip(instance, logger): @@ -74,6 +68,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin): if not validate_otio_clip(instance, self.log): return + import opentimelineio as otio + otio_clip = instance.data["otioClip"] # Collect timeline ranges if workfile start frame is available @@ -100,6 +96,11 @@ class CollectOtioRanges(pyblish.api.InstancePlugin): def _collect_timeline_ranges(self, instance, otio_clip): """Collect basic timeline frame ranges.""" + from ayon_core.pipeline.editorial import ( + otio_range_to_frame_range, + otio_range_with_handles, + ) + workfile_start = instance.data["workfileFrameStart"] # Get timeline ranges @@ -129,6 +130,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin): def _collect_source_ranges(self, instance, otio_clip): """Collect source media frame ranges.""" + import opentimelineio as otio + # Get source ranges otio_src_range = otio_clip.source_range otio_available_range = otio_clip.available_range() @@ -178,6 +181,8 @@ class CollectOtioRanges(pyblish.api.InstancePlugin): def _collect_retimed_ranges(self, instance, otio_clip): """Handle retimed clip frame ranges.""" + from ayon_core.pipeline.editorial import get_media_range_with_retimes + retimed_attributes = get_media_range_with_retimes(otio_clip, 0, 0) self.log.debug(f"Retimed attributes: {retimed_attributes}") 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 2aec4a5415..86d18ed147 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -7,7 +7,6 @@ from ayon_core.lib import ( get_ffmpeg_tool_args, run_subprocess ) -from ayon_core.pipeline import editorial class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): @@ -159,6 +158,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): """ # Not all hosts can import this module. import opentimelineio as otio + from ayon_core.pipeline.editorial import OTIO_EPSILON output = [] # go trough all audio tracks diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 74cf45e474..28452bc0e9 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -25,7 +25,6 @@ from ayon_core.lib import ( ) from ayon_core.pipeline import ( KnownPublishError, - editorial, publish, ) @@ -359,6 +358,7 @@ class ExtractOTIOReview( import opentimelineio as otio from ayon_core.pipeline.editorial import ( trim_media_range, + OTIO_EPSILON, ) def _round_to_frame(rational_time): @@ -380,7 +380,7 @@ class ExtractOTIOReview( # Avoid rounding issue on media available range. if start.almost_equal( avl_start, - editorial.OTIO_EPSILON + OTIO_EPSILON ): avl_start = start From 798b281e6731947cd4700591632f4e4c4134b73b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:42:16 +0200 Subject: [PATCH 47/71] fix OTIO_EPSILON usage --- client/ayon_core/plugins/publish/extract_otio_audio_tracks.py | 2 +- client/ayon_core/plugins/publish/extract_otio_review.py | 2 +- 2 files changed, 2 insertions(+), 2 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 86d18ed147..3a450a4f33 100644 --- a/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py +++ b/client/ayon_core/plugins/publish/extract_otio_audio_tracks.py @@ -177,7 +177,7 @@ class ExtractOtioAudioTracks(pyblish.api.ContextPlugin): # Avoid rounding issue on media available range. if clip_start.almost_equal( conformed_av_start, - editorial.OTIO_EPSILON + OTIO_EPSILON ): conformed_av_start = clip_start diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 28452bc0e9..90215bd2c9 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -406,7 +406,7 @@ class ExtractOTIOReview( # Avoid rounding issue on media available range. if end_point.almost_equal( avl_end_point, - editorial.OTIO_EPSILON + OTIO_EPSILON ): avl_end_point = end_point From 8bcc4a3939d4e52cce731a0e712b87b283fee37d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:23:15 +0200 Subject: [PATCH 48/71] Make sure workdir exists when workfile is being saved --- client/ayon_core/host/interfaces/workfiles.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/host/interfaces/workfiles.py b/client/ayon_core/host/interfaces/workfiles.py index b519751ba2..82d71d152a 100644 --- a/client/ayon_core/host/interfaces/workfiles.py +++ b/client/ayon_core/host/interfaces/workfiles.py @@ -944,6 +944,8 @@ class IWorkfileHost: self._emit_workfile_save_event(event_data, after_save=False) workdir = os.path.dirname(filepath) + if not os.path.exists(workdir): + os.makedirs(workdir, exist_ok=True) # Set 'AYON_WORKDIR' environment variable os.environ["AYON_WORKDIR"] = workdir From 955d8166a5fc4d216368db6ee0a7bf80710f7b32 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Aug 2025 16:36:08 +0000 Subject: [PATCH 49/71] [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 784105572b..b6958f1be5 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.5.1+dev" +__version__ = "1.5.2" diff --git a/package.py b/package.py index a0d7b26703..79fe4f83b1 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.1+dev" +version = "1.5.2" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index b544afa346..73fa4336f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.1+dev" +version = "1.5.2" description = "" authors = ["Ynput Team "] readme = "README.md" From 8cd7037c6f0910ff4ae789dd2b1213b5b5307e85 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 1 Aug 2025 16:36:43 +0000 Subject: [PATCH 50/71] [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 b6958f1be5..9f1bac6805 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.5.2" +__version__ = "1.5.2+dev" diff --git a/package.py b/package.py index 79fe4f83b1..7bd806159f 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.2" +version = "1.5.2+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 73fa4336f1..e67fcc2138 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.2" +version = "1.5.2+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 55f5551b31b1ad2aef3ca801207ad458a19d153e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 1 Aug 2025 16:37:35 +0000 Subject: [PATCH 51/71] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 364d1709e0..933448a6a9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.5.2 - 1.5.1 - 1.5.0 - 1.4.1 From 4f0e18b42ed081370bc7d5279a2a91159a7b139f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:17:50 +0200 Subject: [PATCH 52/71] Remove unnecessary line --- client/ayon_core/addon/interfaces.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index 9f2a14a264..bf08ccd48c 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -99,7 +99,6 @@ class IPluginPaths(AYONInterface): f" '{new_function_name}' instead.", DeprecationWarning, stacklevel=2 - ) return paths From c219403b13c1f9436b17758e095b2f7fdd6788d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:18:10 +0200 Subject: [PATCH 53/71] 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 | 4 +++- 1 file changed, 3 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 6a36fd12e4..9994bcfd4e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -301,7 +301,9 @@ class AbstractTemplateBuilder(ABC): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name - def get_linked_folder_entities(self, link_type: str): + def get_linked_folder_entities(self, link_type: Optional[str]): + if not link_type: + return [] project_name = self.project_name folder_entity = self.current_folder_entity if not folder_entity: From a7a3834fdcc168188d8c944144f8514e24f8bd56 Mon Sep 17 00:00:00 2001 From: Sasbom Date: Fri, 8 Aug 2025 08:31:22 +0200 Subject: [PATCH 54/71] force in UI element from laucher to workfiles --- .../tools/workfiles/widgets/window.py | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 1649a059cb..81f1d76c71 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -1,21 +1,20 @@ -from qtpy import QtCore, QtWidgets, QtGui +from qtpy import QtCore, QtGui, QtWidgets -from ayon_core import style, resources +from ayon_core import resources, style from ayon_core.tools.utils import ( - PlaceholderLineEdit, - MessageOverlayObject, -) - -from ayon_core.tools.workfiles.control import BaseWorkfileController -from ayon_core.tools.utils import ( - GoToCurrentButton, - RefreshButton, FoldersWidget, + GoToCurrentButton, + MessageOverlayObject, + NiceCheckbox, + PlaceholderLineEdit, + RefreshButton, TasksWidget, ) +from ayon_core.tools.utils.lib import checkstate_int_to_enum +from ayon_core.tools.workfiles.control import BaseWorkfileController -from .side_panel import SidePanelWidget from .files_widget import FilesWidget +from .side_panel import SidePanelWidget from .utils import BaseOverlayFrame @@ -186,11 +185,24 @@ class WorkfilesToolWindow(QtWidgets.QWidget): controller, col_widget, handle_expected_selection=True ) + my_tasks_tooltip = ( + "Filter folders and task to only those you are assigned to." + ) + + my_tasks_label = QtWidgets.QLabel("My tasks") + my_tasks_label.setToolTip(my_tasks_tooltip) + + my_tasks_checkbox = NiceCheckbox(folder_widget) + my_tasks_checkbox.setChecked(False) + my_tasks_checkbox.setToolTip(my_tasks_tooltip) + header_layout = QtWidgets.QHBoxLayout(header_widget) header_layout.setContentsMargins(0, 0, 0, 0) header_layout.addWidget(folder_filter_input, 1) header_layout.addWidget(go_to_current_btn, 0) header_layout.addWidget(refresh_btn, 0) + header_layout.addWidget(my_tasks_label, 0) + header_layout.addWidget(my_tasks_checkbox, 0) col_layout = QtWidgets.QVBoxLayout(col_widget) col_layout.setContentsMargins(0, 0, 0, 0) @@ -200,6 +212,9 @@ class WorkfilesToolWindow(QtWidgets.QWidget): folder_filter_input.textChanged.connect(self._on_folder_filter_change) go_to_current_btn.clicked.connect(self._on_go_to_current_clicked) refresh_btn.clicked.connect(self._on_refresh_clicked) + my_tasks_checkbox.stateChanged.connect( + self._on_my_tasks_checkbox_state_changed + ) self._folder_filter_input = folder_filter_input self._folders_widget = folder_widget @@ -385,3 +400,16 @@ class WorkfilesToolWindow(QtWidgets.QWidget): ) else: self.close() + + def _on_my_tasks_checkbox_state_changed(self, state): + folder_ids = None + task_ids = None + state = checkstate_int_to_enum(state) + if state == QtCore.Qt.Checked: + entity_ids = self._controller.get_my_tasks_entity_ids( + self._project_name + ) + folder_ids = entity_ids["folder_ids"] + task_ids = entity_ids["task_ids"] + self._folders_widget.set_folder_ids_filter(folder_ids) + self._tasks_widget.set_task_ids_filter(task_ids) From 20614562cd5a8818191d8daa1822cf354ffa6a86 Mon Sep 17 00:00:00 2001 From: Sasbom Date: Fri, 8 Aug 2025 09:13:31 +0200 Subject: [PATCH 55/71] implement interface for "my task" functionality in workfiles control / window --- client/ayon_core/tools/workfiles/control.py | 26 +++++++++++++++---- .../tools/workfiles/widgets/window.py | 4 +++ 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 4391e6b5fd..f0e0f0e416 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -3,25 +3,26 @@ import os import ayon_api from ayon_core.host import IWorkfileHost -from ayon_core.lib import Logger +from ayon_core.lib import Logger, get_ayon_username from ayon_core.lib.events import QueuedEventSystem -from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy, registered_host from ayon_core.pipeline.context_tools import get_global_context - +from ayon_core.settings import get_project_settings from ayon_core.tools.common_models import ( - HierarchyModel, HierarchyExpectedSelection, + HierarchyModel, ProjectsModel, UsersModel, ) from .abstract import ( - AbstractWorkfilesFrontend, AbstractWorkfilesBackend, + AbstractWorkfilesFrontend, ) from .models import SelectionModel, WorkfilesModel +NOT_SET = object() + class WorkfilesToolExpectedSelection(HierarchyExpectedSelection): def __init__(self, controller): @@ -143,6 +144,7 @@ class BaseWorkfileController( self._project_settings = None self._event_system = None self._log = None + self._username = NOT_SET self._current_project_name = None self._current_folder_path = None @@ -588,6 +590,20 @@ class BaseWorkfileController( description, ) + def get_my_tasks_entity_ids(self, project_name: str): + username = self._get_my_username() + assignees = [] + if username: + assignees.append(username) + return self._hierarchy_model.get_entity_ids_for_assignees( + project_name, assignees + ) + + def _get_my_username(self): + if self._username is NOT_SET: + self._username = get_ayon_username() + return self._username + def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 81f1d76c71..1e78b89851 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -1,3 +1,4 @@ + from qtpy import QtCore, QtGui, QtWidgets from ayon_core import resources, style @@ -156,6 +157,9 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._home_body_widget = home_body_widget self._split_widget = split_widget + host = self._controller._host + self._project_name = host.get_current_project_name() + self._tasks_widget = tasks_widget self._side_panel = side_panel From f4578e93a9f5a8a4f86cad0a4e9510d375b0d707 Mon Sep 17 00:00:00 2001 From: Sasbom Date: Fri, 8 Aug 2025 09:16:32 +0200 Subject: [PATCH 56/71] embiggen first panel to accommodate added ui element --- client/ayon_core/tools/workfiles/widgets/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 1e78b89851..7c00499b2d 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -107,7 +107,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): split_widget.addWidget(tasks_widget) split_widget.addWidget(col_3_widget) split_widget.addWidget(side_panel) - split_widget.setSizes([255, 175, 550, 190]) + split_widget.setSizes([350, 175, 550, 190]) body_layout.addWidget(split_widget) From 0d945c90ecf37c794f84ff7048c0e0474de30bf1 Mon Sep 17 00:00:00 2001 From: Sasbom Date: Fri, 8 Aug 2025 10:17:13 +0200 Subject: [PATCH 57/71] neaten up project name retrieval through canonical means --- client/ayon_core/tools/workfiles/widgets/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 7c00499b2d..3f96f0bb15 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -157,8 +157,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): self._home_body_widget = home_body_widget self._split_widget = split_widget - host = self._controller._host - self._project_name = host.get_current_project_name() + self._project_name = self._controller.get_current_project_name() self._tasks_widget = tasks_widget self._side_panel = side_panel From 80cd3a3ea811dc70dd2e0c44b22eb42ebb8d4d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 8 Aug 2025 12:23:43 +0200 Subject: [PATCH 58/71] :bug: fix import --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 9994bcfd4e..e2add99752 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -16,6 +16,7 @@ import re import collections import copy from abc import ABC, abstractmethod +from typing import Optional import ayon_api from ayon_api import ( From f11fe9c089b775d431d458c15d1022b24d9a7c2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:52:10 +0200 Subject: [PATCH 59/71] allow copy of published workfile without task --- client/ayon_core/tools/workfiles/widgets/files_widget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/files_widget.py b/client/ayon_core/tools/workfiles/widgets/files_widget.py index 0c8ad392e2..9c12fa575c 100644 --- a/client/ayon_core/tools/workfiles/widgets/files_widget.py +++ b/client/ayon_core/tools/workfiles/widgets/files_widget.py @@ -287,10 +287,11 @@ class FilesWidget(QtWidgets.QWidget): def _update_published_btns_state(self): enabled = ( self._valid_representation_id - and self._valid_selected_context and self._is_save_enabled ) - self._published_btn_copy_n_open.setEnabled(enabled) + self._published_btn_copy_n_open.setEnabled( + enabled and self._valid_selected_context + ) self._published_btn_change_context.setEnabled(enabled) def _update_workarea_btns_state(self): From 277489b4252464230ff2574f8287f334149fa03b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Aug 2025 12:19:27 +0000 Subject: [PATCH 60/71] [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 9f1bac6805..11cbfa61b5 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.5.2+dev" +__version__ = "1.5.3" diff --git a/package.py b/package.py index 7bd806159f..012bbd081c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.2+dev" +version = "1.5.3" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index e67fcc2138..91748f801b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.2+dev" +version = "1.5.3" description = "" authors = ["Ynput Team "] readme = "README.md" From c8f802b210026bb0b47c85efea4fe1d23c516835 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 13 Aug 2025 12:20:11 +0000 Subject: [PATCH 61/71] [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 11cbfa61b5..f2aa94020f 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.5.3" +__version__ = "1.5.3+dev" diff --git a/package.py b/package.py index 012bbd081c..07a1246c9f 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.5.3" +version = "1.5.3+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 91748f801b..ee6c35b50b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.5.3" +version = "1.5.3+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 3ef40e203bafbd229cd21417b604a939fc1cd9fb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 Aug 2025 12:21:07 +0000 Subject: [PATCH 62/71] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 933448a6a9..ce5982969c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.5.3 - 1.5.2 - 1.5.1 - 1.5.0 From 4951c9442a86bc23bd8ffa40618fc9c9676a8c49 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:45:22 +0200 Subject: [PATCH 63/71] small enhancements in nice checkbox --- client/ayon_core/tools/utils/nice_checkbox.py | 200 +++++++++--------- 1 file changed, 100 insertions(+), 100 deletions(-) diff --git a/client/ayon_core/tools/utils/nice_checkbox.py b/client/ayon_core/tools/utils/nice_checkbox.py index 3d9d63b6bc..d1cc8d16f5 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -1,7 +1,8 @@ -from math import floor, sqrt, ceil +from math import floor, ceil + from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.style import get_objected_colors +from ayon_core.style import load_stylesheet, get_objected_colors class NiceCheckbox(QtWidgets.QFrame): @@ -9,12 +10,15 @@ class NiceCheckbox(QtWidgets.QFrame): clicked = QtCore.Signal() _checked_bg_color = None + _checked_bg_color_disabled = None _unchecked_bg_color = None + _unchecked_bg_color_disabled = None _checker_color = None + _checker_color_disabled = None _checker_hover_color = None def __init__(self, checked=False, draw_icons=False, parent=None): - super(NiceCheckbox, self).__init__(parent) + super().__init__(parent) self.setObjectName("NiceCheckbox") self.setAttribute(QtCore.Qt.WA_TranslucentBackground) @@ -48,8 +52,6 @@ class NiceCheckbox(QtWidgets.QFrame): self._pressed = False self._under_mouse = False - self.icon_scale_factor = sqrt(2) / 2 - icon_path_stroker = QtGui.QPainterPathStroker() icon_path_stroker.setCapStyle(QtCore.Qt.RoundCap) icon_path_stroker.setJoinStyle(QtCore.Qt.RoundJoin) @@ -61,35 +63,6 @@ class NiceCheckbox(QtWidgets.QFrame): self._base_size = QtCore.QSize(90, 50) self._load_colors() - @classmethod - def _load_colors(cls): - if cls._checked_bg_color is not None: - return - - colors_info = get_objected_colors("nice-checkbox") - - cls._checked_bg_color = colors_info["bg-checked"].get_qcolor() - cls._unchecked_bg_color = colors_info["bg-unchecked"].get_qcolor() - - cls._checker_color = colors_info["bg-checker"].get_qcolor() - cls._checker_hover_color = colors_info["bg-checker-hover"].get_qcolor() - - @property - def checked_bg_color(self): - return self._checked_bg_color - - @property - def unchecked_bg_color(self): - return self._unchecked_bg_color - - @property - def checker_color(self): - return self._checker_color - - @property - def checker_hover_color(self): - return self._checker_hover_color - def setTristate(self, tristate=True): if self._is_tristate != tristate: self._is_tristate = tristate @@ -121,14 +94,14 @@ class NiceCheckbox(QtWidgets.QFrame): def setFixedHeight(self, *args, **kwargs): self._fixed_height_set = True - super(NiceCheckbox, self).setFixedHeight(*args, **kwargs) + super().setFixedHeight(*args, **kwargs) if not self._fixed_width_set: width = self.get_width_hint_by_height(self.height()) self.setFixedWidth(width) def setFixedWidth(self, *args, **kwargs): self._fixed_width_set = True - super(NiceCheckbox, self).setFixedWidth(*args, **kwargs) + super().setFixedWidth(*args, **kwargs) if not self._fixed_height_set: height = self.get_height_hint_by_width(self.width()) self.setFixedHeight(height) @@ -136,7 +109,7 @@ class NiceCheckbox(QtWidgets.QFrame): def setFixedSize(self, *args, **kwargs): self._fixed_height_set = True self._fixed_width_set = True - super(NiceCheckbox, self).setFixedSize(*args, **kwargs) + super().setFixedSize(*args, **kwargs) def steps(self): return self._steps @@ -242,7 +215,7 @@ class NiceCheckbox(QtWidgets.QFrame): if event.buttons() & QtCore.Qt.LeftButton: self._pressed = True self.repaint() - super(NiceCheckbox, self).mousePressEvent(event) + super().mousePressEvent(event) def mouseReleaseEvent(self, event): if self._pressed and not event.buttons() & QtCore.Qt.LeftButton: @@ -252,7 +225,7 @@ class NiceCheckbox(QtWidgets.QFrame): self.clicked.emit() event.accept() return - super(NiceCheckbox, self).mouseReleaseEvent(event) + super().mouseReleaseEvent(event) def mouseMoveEvent(self, event): if self._pressed: @@ -261,19 +234,19 @@ class NiceCheckbox(QtWidgets.QFrame): self._under_mouse = under_mouse self.repaint() - super(NiceCheckbox, self).mouseMoveEvent(event) + super().mouseMoveEvent(event) def enterEvent(self, event): self._under_mouse = True if self.isEnabled(): self.repaint() - super(NiceCheckbox, self).enterEvent(event) + super().enterEvent(event) def leaveEvent(self, event): self._under_mouse = False if self.isEnabled(): self.repaint() - super(NiceCheckbox, self).leaveEvent(event) + super().leaveEvent(event) def _on_animation_timeout(self): if self._checkstate == QtCore.Qt.Checked: @@ -302,24 +275,13 @@ class NiceCheckbox(QtWidgets.QFrame): @staticmethod def steped_color(color1, color2, offset_ratio): - red_dif = ( - color1.red() - color2.red() - ) - green_dif = ( - color1.green() - color2.green() - ) - blue_dif = ( - color1.blue() - color2.blue() - ) - red = int(color2.red() + ( - red_dif * offset_ratio - )) - green = int(color2.green() + ( - green_dif * offset_ratio - )) - blue = int(color2.blue() + ( - blue_dif * offset_ratio - )) + red_dif = color1.red() - color2.red() + green_dif = color1.green() - color2.green() + blue_dif = color1.blue() - color2.blue() + + red = int(color2.red() + (red_dif * offset_ratio)) + green = int(color2.green() + (green_dif * offset_ratio)) + blue = int(color2.blue() + (blue_dif * offset_ratio)) return QtGui.QColor(red, green, blue) @@ -334,20 +296,28 @@ class NiceCheckbox(QtWidgets.QFrame): painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.setPen(QtCore.Qt.NoPen) # Draw inner background - if self._current_step == self._steps: - bg_color = self.checked_bg_color + if not self.isEnabled(): + bg_color = ( + self._checked_bg_color_disabled + if self._current_step == self._steps + else self._unchecked_bg_color_disabled + ) + + elif self._current_step == self._steps: + bg_color = self._checked_bg_color elif self._current_step == 0: - bg_color = self.unchecked_bg_color + bg_color = self._unchecked_bg_color else: offset_ratio = float(self._current_step) / self._steps # Animation bg bg_color = self.steped_color( - self.checked_bg_color, - self.unchecked_bg_color, + self._checked_bg_color, + self._unchecked_bg_color, offset_ratio ) @@ -378,14 +348,20 @@ class NiceCheckbox(QtWidgets.QFrame): -margin_size_c, -margin_size_c ) - if checkbox_rect.width() > checkbox_rect.height(): - radius = floor(checkbox_rect.height() * 0.5) - else: - radius = floor(checkbox_rect.width() * 0.5) + slider_rect = QtCore.QRect(checkbox_rect) + slider_offset = int( + ceil(min(slider_rect.width(), slider_rect.height())) * 0.08 + ) + if slider_offset < 1: + slider_offset = 1 + slider_rect.adjust( + slider_offset, slider_offset, + -slider_offset, -slider_offset + ) + radius = floor(min(slider_rect.width(), slider_rect.height()) * 0.5) - painter.setPen(QtCore.Qt.NoPen) painter.setBrush(bg_color) - painter.drawRoundedRect(checkbox_rect, radius, radius) + painter.drawRoundedRect(slider_rect, radius, radius) # Draw checker checker_size = size_without_margins - (margin_size_c * 2) @@ -394,9 +370,8 @@ class NiceCheckbox(QtWidgets.QFrame): - (margin_size_c * 2) - checker_size ) - if self._current_step == 0: - x_offset = 0 - else: + x_offset = 0 + if self._current_step != 0: x_offset = (float(area_width) / self._steps) * self._current_step pos_x = checkbox_rect.x() + x_offset + margin_size_c @@ -404,55 +379,80 @@ class NiceCheckbox(QtWidgets.QFrame): checker_rect = QtCore.QRect(pos_x, pos_y, checker_size, checker_size) - under_mouse = self.isEnabled() and self._under_mouse - if under_mouse: - checker_color = self.checker_hover_color - else: - checker_color = self.checker_color + checker_color = self._checker_color + if not self.isEnabled(): + checker_color = self._checker_color_disabled + elif self._under_mouse: + checker_color = self._checker_hover_color painter.setBrush(checker_color) painter.drawEllipse(checker_rect) if self._draw_icons: painter.setBrush(bg_color) - icon_path = self._get_icon_path(painter, checker_rect) + icon_path = self._get_icon_path(checker_rect) painter.drawPath(icon_path) - # Draw shadow overlay - if not self.isEnabled(): - level = 33 - alpha = 127 - painter.setPen(QtCore.Qt.transparent) - painter.setBrush(QtGui.QColor(level, level, level, alpha)) - painter.drawRoundedRect(checkbox_rect, radius, radius) - painter.end() - def _get_icon_path(self, painter, checker_rect): + @classmethod + def _load_colors(cls): + if cls._checked_bg_color is not None: + return + + colors_info = get_objected_colors("nice-checkbox") + + disabled_color = QtGui.QColor(33, 33, 33, 127) + + cls._checked_bg_color = colors_info["bg-checked"].get_qcolor() + cls._checked_bg_color_disabled = cls._merge_colors( + cls._checked_bg_color, disabled_color + ) + cls._unchecked_bg_color = colors_info["bg-unchecked"].get_qcolor() + cls._unchecked_bg_color_disabled = cls._merge_colors( + cls._unchecked_bg_color, disabled_color + ) + + cls._checker_color = colors_info["bg-checker"].get_qcolor() + cls._checker_color_disabled = cls._merge_colors( + cls._checker_color, disabled_color + ) + cls._checker_hover_color = colors_info["bg-checker-hover"].get_qcolor() + + @staticmethod + def _merge_colors(color_1, color_2): + a = color_2.alphaF() + return QtGui.QColor( + floor((color_1.red() + (color_2.red() * a)) * 0.5), + floor((color_1.green() + (color_2.green() * a)) * 0.5), + floor((color_1.blue() + (color_2.blue() * a)) * 0.5), + color_1.alpha() + ) + + def _get_icon_path(self, checker_rect): self.icon_path_stroker.setWidth(checker_rect.height() / 5) if self._current_step == self._steps: - return self._get_enabled_icon_path(painter, checker_rect) + return self._get_enabled_icon_path(checker_rect) if self._current_step == 0: - return self._get_disabled_icon_path(painter, checker_rect) + return self._get_disabled_icon_path(checker_rect) if self._current_step == self._middle_step: - return self._get_middle_circle_path(painter, checker_rect) + return self._get_middle_circle_path(checker_rect) disabled_step = self._steps - self._current_step enabled_step = self._steps - disabled_step half_steps = self._steps + 1 - ((self._steps + 1) % 2) if enabled_step > disabled_step: return self._get_enabled_icon_path( - painter, checker_rect, enabled_step, half_steps - ) - else: - return self._get_disabled_icon_path( - painter, checker_rect, disabled_step, half_steps + checker_rect, enabled_step, half_steps ) + return self._get_disabled_icon_path( + checker_rect, disabled_step, half_steps + ) - def _get_middle_circle_path(self, painter, checker_rect): + def _get_middle_circle_path(self, checker_rect): width = self.icon_path_stroker.width() path = QtGui.QPainterPath() path.addEllipse(checker_rect.center(), width, width) @@ -460,7 +460,7 @@ class NiceCheckbox(QtWidgets.QFrame): return path def _get_enabled_icon_path( - self, painter, checker_rect, step=None, half_steps=None + self, checker_rect, step=None, half_steps=None ): fifteenth = float(checker_rect.height()) / 15 # Left point @@ -509,7 +509,7 @@ class NiceCheckbox(QtWidgets.QFrame): return self.icon_path_stroker.createStroke(path) def _get_disabled_icon_path( - self, painter, checker_rect, step=None, half_steps=None + self, checker_rect, step=None, half_steps=None ): center_point = QtCore.QPointF( float(checker_rect.width()) / 2, From f4855402cf82e44554df9d471d82366c2637a2eb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:50:38 +0200 Subject: [PATCH 64/71] remove unused import --- 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 d1cc8d16f5..f16b62eb9c 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -2,7 +2,7 @@ from math import floor, ceil from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.style import load_stylesheet, get_objected_colors +from ayon_core.style import get_objected_colors class NiceCheckbox(QtWidgets.QFrame): From 40e8384b1cb33c0771e62eba7b0dedaac9e28e94 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:50:44 +0200 Subject: [PATCH 65/71] formatting fix --- 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 f16b62eb9c..c33533b0e4 100644 --- a/client/ayon_core/tools/utils/nice_checkbox.py +++ b/client/ayon_core/tools/utils/nice_checkbox.py @@ -358,7 +358,7 @@ class NiceCheckbox(QtWidgets.QFrame): slider_offset, slider_offset, -slider_offset, -slider_offset ) - radius = floor(min(slider_rect.width(), slider_rect.height()) * 0.5) + radius = floor(min(slider_rect.width(), slider_rect.height()) * 0.5) painter.setBrush(bg_color) painter.drawRoundedRect(slider_rect, radius, radius) From 6c6be3508f5292324123fad320d78d98644b7de9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:53:14 +0200 Subject: [PATCH 66/71] Propagate taskId to limit json parse issue --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) 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 b180892d62..0c654a5495 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1169,6 +1169,7 @@ class ProjectPushItemProcess: self._operations.update_version( project_name=self._item.dst_project_name, version_id=self._version_entity["id"], + task_id=self._version_entity.get("taskId"), thumbnail_id=new_thumbnail_id ) self._operations.commit() From 7860c7d875c539f52bfdd78c590ebc77a80c5af6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 13:55:49 +0200 Subject: [PATCH 67/71] Removed unnecessary refresh --- client/ayon_core/tools/push_to_project/ui/window.py | 2 -- 1 file changed, 2 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 38c343b023..495ef83ce6 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -408,8 +408,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): """Change toggle state, reset filter, recalculate dropdown""" state = bool(state) self._projects_combobox.set_standard_filter_enabled(state) - self._projects_combobox.refresh() - def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From d79bd055bcf771553076016b5cfd02e9ad4c5468 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 14:03:08 +0200 Subject: [PATCH 68/71] Removed unnecessary import --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 - 1 file changed, 1 deletion(-) 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 0c654a5495..ac2f506112 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -3,7 +3,6 @@ import re import copy import itertools import sys -import tempfile import traceback import uuid From 930439ba12990611d8b01a7114fc4cdd5a77dab2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 14:26:28 +0200 Subject: [PATCH 69/71] Fix copy of frames based representations --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 - 1 file changed, 1 deletion(-) 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 ac2f506112..40c418e513 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -372,7 +372,6 @@ class ProjectPushRepreItem: resource_files.append(ResourceFile(filepath, relative_path)) continue - filepath = os.path.join(src_dirpath, basename) frame = None udim = None for item in src_basename_regex.finditer(basename): From 18ad64e2260124407cef7d01d37e5ceba0527f20 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 14:43:29 +0200 Subject: [PATCH 70/71] Updated _copy_version_thumbnail logic --- .../tools/push_to_project/models/integrate.py | 18 ++++++++---------- 1 file changed, 8 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 40c418e513..08fafcbf2d 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -484,7 +484,6 @@ class ProjectPushItemProcess: self._make_sure_version_exists() self._log_info("Prerequirements were prepared") self._integrate_representations() - self._copy_version_thumbnail() self._log_info("Integration finished") except PushToProjectError as exc: @@ -918,14 +917,19 @@ class ProjectPushItemProcess: task_name=self._task_info["name"], task_type=self._task_info["taskType"], product_type=product_type, - product_name=product_entity["name"] + product_name=product_entity["name"], ) existing_version_entity = ayon_api.get_version_by_name( project_name, version, product_id ) + thumbnail_id = self._copy_version_thumbnail() + # Update existing version if existing_version_entity: + updata_data = {"attrib": dst_attrib} + if thumbnail_id: + updata_data["thumbnailId"] = thumbnail_id self._operations.update_entity( project_name, "version", @@ -940,6 +944,7 @@ class ProjectPushItemProcess: version, product_id, attribs=dst_attrib, + thumbnail_id=thumbnail_id, ) self._operations.create_entity( project_name, "version", version_entity @@ -1160,17 +1165,10 @@ class ProjectPushItemProcess: ) if not path: return - new_thumbnail_id = ayon_api.create_thumbnail( + return ayon_api.create_thumbnail( self._item.dst_project_name, path ) - self._operations.update_version( - project_name=self._item.dst_project_name, - version_id=self._version_entity["id"], - task_id=self._version_entity.get("taskId"), - thumbnail_id=new_thumbnail_id - ) - self._operations.commit() class IntegrateModel: From c2b6204c0a1c10b463e9810009015b3e326df8ba Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 18 Aug 2025 15:09:17 +0200 Subject: [PATCH 71/71] Formatting change --- client/ayon_core/tools/push_to_project/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index b52eeb5fad..fb080d158b 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -40,7 +40,6 @@ class PushToContextController: self.set_source(project_name, version_id) - # Events system def emit_event(self, topic, data=None, source=None): """Use implemented event system to trigger event."""