From b941c3169400601afcd14e7556581b4c123da561 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 May 2026 13:19:05 +0200 Subject: [PATCH 1/7] move some logic to utils --- server/__init__.py | 11 +++++ server/actions.py | 88 ++++++--------------------------- server/utils.py | 120 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 74 deletions(-) create mode 100644 server/utils.py diff --git a/server/__init__.py b/server/__init__.py index 8ecffad159..104c7d2a2e 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -1,6 +1,17 @@ from .addon import ApplicationsAddon +from .utils import ( + ApplicationItem, + ToolItem, + get_application_items, + get_tool_items, +) __all__ = ( "ApplicationsAddon", + + "ApplicationItem", + "ToolItem", + "get_application_items", + "get_tool_items", ) diff --git a/server/actions.py b/server/actions.py index 8386b0a0b7..1624959a04 100644 --- a/server/actions.py +++ b/server/actions.py @@ -1,5 +1,4 @@ import collections -import os import copy import typing from typing import Any @@ -17,7 +16,7 @@ except ImportError: SimpleForm = None -from .constants import LABELS_BY_GROUP_NAME, ICONS_BY_GROUP_NAME +from .utils import get_application_items, ApplicationItem IDENTIFIER_PREFIX = "application.launch." IDENTIFIER_WORKFILE_PREFIX = "application.launch-workfile." @@ -33,88 +32,29 @@ from .addon import ApplicationsAddon -def _sort_getter(item): - return item["group_label"], item["variant_label"] - - -def get_items_for_app_groups(groups): - items = [] - for group in groups: - group_name = group["name"] - group_label = group.get( - "label", LABELS_BY_GROUP_NAME.get(group_name) - ) or group_name - icon_name = ICONS_BY_GROUP_NAME.get(group_name) - if not icon_name: - icon_name = group.get("icon") - - if icon_name: - icon_name = os.path.basename(icon_name) - - icon = None - if icon_name: - icon = { - "type": "url", - "url": "{addon_url}/public/icons/" + icon_name, - } - - for variant in group["variants"]: - variant_name = variant["name"] - if not variant_name: - continue - variant_group_label = variant["group_label"] - if not variant_group_label: - variant_group_label = group_label - variant_label = variant["label"] or variant_name - full_name = f"{group_name}/{variant_name}" - items.append({ - "host_name": group["host_name"], - "value": full_name, - "group_label": variant_group_label, - "variant_label": variant_label, - "icon": icon, - "show_grouped": variant["show_grouped"], - }) - - items.sort(key=_sort_getter) - return items - - -def _prepare_label_kwargs(item): - group_label = item["group_label"] - variant_label = item["variant_label"] - if _GROUP_LABEL_AVAILABLE and item["show_grouped"]: +def _prepare_label_kwargs(item: ApplicationItem) -> dict[str, str]: + if _GROUP_LABEL_AVAILABLE and item.show_grouped: return { - "label": variant_label, - "group_label": group_label, + "label": item.variant_label, + "group_label": item.group_label, } return { - "label": f"{group_label} {variant_label}", + "label": item.full_label, } def _get_app_items_by_name( addon_settings: dict[str, Any] -) -> dict[str, dict[str, Any]]: - app_settings = addon_settings["applications"] - app_groups = app_settings.pop("additional_apps") - for group_name, value in app_settings.items(): - if not value["enabled"]: - continue - value["name"] = group_name - app_groups.append(value) - - # This is very simplified profiles logic - app_items = get_items_for_app_groups(app_groups) +) -> dict[str, ApplicationItem]: return { - item["value"]: item - for item in app_items + item.full_name: item + for item in get_application_items(addon_settings) } def _get_task_types_by_app_name( - app_items_by_name: dict[str, dict[str, Any]], + app_items_by_name: dict[str, ApplicationItem], addon_settings: dict[str, Any], project_entity: ProjectEntity ) -> dict[str, set[str]]: @@ -127,7 +67,7 @@ def _get_task_types_by_app_name( project_apps = project_entity.original_attributes.get( "applications", [] ) - for app_full_name, item in app_items_by_name.items(): + for app_full_name in app_items_by_name.keys(): if app_full_name in project_apps: task_types_by_app_name[app_full_name] |= ( project_task_types.copy() @@ -222,7 +162,7 @@ async def get_action_manifests( identifier=f"{IDENTIFIER_PREFIX}{app_name}", **_prepare_label_kwargs(app_item), category="Applications", - icon=app_item["icon"], + icon=app_item.icon, order=0, entity_type="task", entity_subtypes=list(task_types), @@ -299,13 +239,13 @@ async def get_dynamic_action_manifests( collected_apps.add(app_name) app_item = app_items_by_name[app_name] - if app_item["host_name"] not in host_names: + if app_item.host_name not in host_names: continue output.append(DynamicActionManifest( identifier=f"{IDENTIFIER_WORKFILE_PREFIX}{app_name}", **_prepare_label_kwargs(app_item), category="Applications", - icon=app_item["icon"], + icon=app_item.icon, order=0, addon_name=addon.name, addon_version=addon.version, diff --git a/server/utils.py b/server/utils.py new file mode 100644 index 0000000000..0fae9acb4d --- /dev/null +++ b/server/utils.py @@ -0,0 +1,120 @@ +from dataclasses import dataclass +import os +from typing import Any + +from .constants import LABELS_BY_GROUP_NAME, ICONS_BY_GROUP_NAME + + +@dataclass +class ApplicationItem: + host_name: str + full_name: str + full_label: str + group_label: str + variant_label: str + icon: dict[str, str] | None + show_grouped: bool + + +@dataclass +class ToolItem: + full_name: str + full_label: str + group_label: str + variant_label: str + host_names: list[str] + app_variants: list[str] + + +def get_application_items( + addon_settings: dict[str, Any] +) -> list[ApplicationItem]: + app_settings = addon_settings["applications"] + app_groups = app_settings.pop("additional_apps") + for group_name, value in app_settings.items(): + if not value["enabled"]: + continue + value["name"] = group_name + app_groups.append(value) + + return get_items_for_app_groups(app_groups) + +def get_tool_items( + addon_settings: dict[str, Any] +) -> list[ToolItem]: + return get_items_for_tool_groups(addon_settings["tool_groups"]) + + +def _sort_getter(item: ApplicationItem): + return item.group_label, item.variant_label + + +def get_items_for_app_groups(groups): + items = [] + for group in groups: + group_name = group["name"] + group_label = group.get( + "label", LABELS_BY_GROUP_NAME.get(group_name) + ) or group_name + icon_name = ICONS_BY_GROUP_NAME.get(group_name) + if not icon_name: + icon_name = group.get("icon") + + if icon_name: + icon_name = os.path.basename(icon_name) + + icon = None + if icon_name: + icon = { + "type": "url", + "url": "{addon_url}/public/icons/" + icon_name, + } + + for variant in group["variants"]: + variant_name = variant["name"] + if not variant_name: + continue + variant_group_label = variant["group_label"] + if not variant_group_label: + variant_group_label = group_label + variant_label = variant["label"] or variant_name + full_label = f"{variant_group_label} {variant_label}" + full_name = f"{group_name}/{variant_name}" + items.append(ApplicationItem( + host_name=group["host_name"], + full_name=full_name, + full_label=full_label, + group_label=variant_group_label, + variant_label=variant_label, + icon=icon, + show_grouped=variant["show_grouped"], + )) + + items.sort(key=_sort_getter) + return items + + +def get_items_for_tool_groups(groups): + items = [] + for group in groups: + group_name = group["name"] + group_label = group["label"] or group_name + + for variant in group["variants"]: + variant_name = variant["name"] + if not variant_name: + continue + variant_label = variant["label"] or variant_name + full_label = f"{group_label} {variant_label}" + full_name = f"{group_name}/{variant_name}" + items.append(ToolItem( + full_name=full_name, + full_label=full_label, + group_label=group_label, + variant_label=variant_label, + host_names=list(variant["host_names"]), + app_variants=list(variant["app_variants"]), + )) + + items.sort(key=_sort_getter) + return items From ebc42fd744324cf9d73f36916e6bd1e95ae1e256 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 May 2026 13:19:17 +0200 Subject: [PATCH 2/7] add public api on the addon --- server/addon.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/server/addon.py b/server/addon.py index ee9fe67945..11086edd8f 100644 --- a/server/addon.py +++ b/server/addon.py @@ -37,6 +37,10 @@ from ayon_server.entities.core import attribute_library from ayon_server.entities.user import UserEntity from ayon_server.helpers.project_list import get_project_list +from ayon_server.bundles.project_bundles import ( + has_project_bundle, + get_project_bundle_addons, +) try: # Added in ayon-backend 1.8.0 @@ -59,6 +63,12 @@ def hash_data(data): from .constants import LABELS_BY_GROUP_NAME from .settings import ApplicationsAddonSettings, DEFAULT_VALUES +from .utils import ( + ApplicationItem, + ToolItem, + get_application_items, + get_tool_items, +) from .actions import ( get_action_manifests, get_dynamic_action_manifests, @@ -342,6 +352,64 @@ async def convert_settings_overrides( prj_tools["enabled"] = False return overrides + async def get_application_items( + self, project_name: str | None, variant: str + ) -> list[ApplicationItem]: + if project_name is None: + settings = await self.get_studio_settings(variant=variant) + else: + settings = await self.get_project_settings( + project_name, variant=variant + ) + return get_application_items(settings.dict()) + + async def get_tool_items( + self, project_name: str | None, variant: str + ) -> list[ToolItem]: + if project_name is None: + settings = await self.get_studio_settings(variant=variant) + else: + settings = await self.get_project_settings( + project_name, variant=variant + ) + return get_tool_items(settings.dict()) + + async def get_applications_for_context( + self, project_name: str | None, variant: str + ) -> list[ApplicationItem]: + """Get applications available for a given context. + + This method can be used by other addons to get applciations available + for a given project and variant. It will return applciations based + on variant and project bundle if project has any. + + Will work only if the addon version is new enough to have + 'get_tool_items' method, otherwise it will return empty list. + + """ + addon = await self._get_addon_for_context(project_name, variant) + if hasattr(addon, "get_application_items"): + return await addon.get_application_items(project_name, variant) + return [] + + async def get_tools_for_context( + self, project_name: str | None, variant: str + ) -> list[ToolItem]: + """Get tools available for a given context. + + This method can be used by other addons to get tools available for + a given project and variant. It will return tools based on variant + and project bundle if project has any. + + Will work only if the addon version is new enough to have + 'get_tool_items' method, otherwise it will return empty list. + + """ + addon = await self._get_addon_for_context(project_name, variant) + if hasattr(addon, "get_tool_items"): + return await addon.get_tool_items(project_name, variant) + return [] + # -------------------------------------- # Backwards compatibility for attributes # -------------------------------------- @@ -591,3 +659,38 @@ async def _autofill_workfile_entities(self): "project_names": project_names, } ) + + async def _get_studio_bundle_addon(self, variant: str): + addon_library = AddonLibrary.getinstance() + if (addon_def := addon_library.data.get(self.name)) is None: + return None + addon_versions_by_name = ( + await addon_library.get_addon_versions_by_variant(variant) + ) + version = addon_versions_by_name.get(self.name) + return addon_def.get(version) + + async def _get_addon_for_context( + self, project_name: str | None, variant: str + ): + if ( + project_name is None + or variant not in ("production", "staging") + or not await has_project_bundle(project_name, variant=variant) + ): + return await self._get_studio_bundle_addon(variant) + + addons = await get_project_bundle_addons( + project_name, variant=variant + ) + version = addons.get(self.name) + if not version or version == "__disable__": + return None + + if version == "__inherit__": + return await self._get_studio_bundle_addon(variant) + + addon_library = AddonLibrary.getinstance() + if (addon_def := addon_library.data.get(self.name)) is None: + return None + return addon_def.get(version) From fa9e835f6bf175025db839ffade26a515df84a72 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 May 2026 13:21:25 +0200 Subject: [PATCH 3/7] make the addon version getter public function --- server/addon.py | 51 +++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/server/addon.py b/server/addon.py index 11086edd8f..5d3ed4499b 100644 --- a/server/addon.py +++ b/server/addon.py @@ -374,6 +374,32 @@ async def get_tool_items( ) return get_tool_items(settings.dict()) + async def get_addon_for_context( + self, project_name: str | None, variant: str + ) -> "ApplicationsAddon" | None: + """Find applications addon version for a given context.""" + if ( + project_name is None + or variant not in ("production", "staging") + or not await has_project_bundle(project_name, variant=variant) + ): + return await self._get_studio_bundle_addon(variant) + + addons = await get_project_bundle_addons( + project_name, variant=variant + ) + version = addons.get(self.name) + if not version or version == "__disable__": + return None + + if version == "__inherit__": + return await self._get_studio_bundle_addon(variant) + + addon_library = AddonLibrary.getinstance() + if (addon_def := addon_library.data.get(self.name)) is None: + return None + return addon_def.get(version) + async def get_applications_for_context( self, project_name: str | None, variant: str ) -> list[ApplicationItem]: @@ -669,28 +695,3 @@ async def _get_studio_bundle_addon(self, variant: str): ) version = addon_versions_by_name.get(self.name) return addon_def.get(version) - - async def _get_addon_for_context( - self, project_name: str | None, variant: str - ): - if ( - project_name is None - or variant not in ("production", "staging") - or not await has_project_bundle(project_name, variant=variant) - ): - return await self._get_studio_bundle_addon(variant) - - addons = await get_project_bundle_addons( - project_name, variant=variant - ) - version = addons.get(self.name) - if not version or version == "__disable__": - return None - - if version == "__inherit__": - return await self._get_studio_bundle_addon(variant) - - addon_library = AddonLibrary.getinstance() - if (addon_def := addon_library.data.get(self.name)) is None: - return None - return addon_def.get(version) From e0b7f2f51537c0d82c3eb46d795161b4a8b7758f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 May 2026 16:31:50 +0200 Subject: [PATCH 4/7] add missing line --- server/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/utils.py b/server/utils.py index 0fae9acb4d..a1da855741 100644 --- a/server/utils.py +++ b/server/utils.py @@ -39,6 +39,7 @@ def get_application_items( return get_items_for_app_groups(app_groups) + def get_tool_items( addon_settings: dict[str, Any] ) -> list[ToolItem]: From cb5308b9398163304cdb0a5f7d23ae5389214a2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 May 2026 12:40:29 +0200 Subject: [PATCH 5/7] change typehint --- server/addon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/addon.py b/server/addon.py index 5d3ed4499b..45158b075c 100644 --- a/server/addon.py +++ b/server/addon.py @@ -376,7 +376,7 @@ async def get_tool_items( async def get_addon_for_context( self, project_name: str | None, variant: str - ) -> "ApplicationsAddon" | None: + ) -> BaseServerAddon | None: """Find applications addon version for a given context.""" if ( project_name is None From ec2d5eee0064ac1e05fe21c053e41054cb9743f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 May 2026 13:08:27 +0200 Subject: [PATCH 6/7] added helper 'get_applications_settings_enum' --- server/addon.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/server/addon.py b/server/addon.py index 45158b075c..f8607f99f1 100644 --- a/server/addon.py +++ b/server/addon.py @@ -62,7 +62,11 @@ def hash_data(data): ) from .constants import LABELS_BY_GROUP_NAME -from .settings import ApplicationsAddonSettings, DEFAULT_VALUES +from .settings import ( + ApplicationsAddonSettings, + DEFAULT_VALUES, + applications_enum, +) from .utils import ( ApplicationItem, ToolItem, @@ -400,6 +404,57 @@ async def get_addon_for_context( return None return addon_def.get(version) + async def get_applications_settings_enum( + self, + *, + project_name: str | None = None, + settings_variant: str = None, + ): + """Helper that can be used to get applications enum for settings. + + Example: + from ayon_server.addons import AddonLibrary + + async def apps_enum(project_name, addon, settings_variant): + addon_library = AddonLibrary.getinstance() + app_addons = addon_library.data.get("applications") or {} + for addon in app_addons.values(): + if not hasattr(addon, "get_applications_settings_enum"): + continue + return await addon.get_applications_settings_enum( + project_name=project_name, + settings_variant=settings_variant, + ) + return [] + + class SomeSettingsModel(BaseModel): + application: str = SettingsField( + default_factory=list, + title="Applications", + enum_resolver=apps_enum, + ) + """ + if settings_variant is None: + settings_variant = "production" + addon = await self.get_addon_for_context( + project_name, settings_variant + ) + if addon is self: + return await applications_enum( + project_name=project_name, + addon=addon, + settings_variant=settings_variant, + ) + + if hasattr(addon, "get_applications_settings_enum"): + v_enum_func = addon.get_applications_settings_enum() + return await v_enum_func( + project_name=project_name, + addon=addon, + settings_variant=settings_variant, + ) + return [] + async def get_applications_for_context( self, project_name: str | None, variant: str ) -> list[ApplicationItem]: From 72ff0ba9ffb81d8e7b1d73481eadeeee7edb9178 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 14:43:19 +0200 Subject: [PATCH 7/7] fix example --- server/addon.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/addon.py b/server/addon.py index f8607f99f1..51a987e03e 100644 --- a/server/addon.py +++ b/server/addon.py @@ -418,9 +418,8 @@ async def get_applications_settings_enum( async def apps_enum(project_name, addon, settings_variant): addon_library = AddonLibrary.getinstance() app_addons = addon_library.data.get("applications") or {} - for addon in app_addons.values(): - if not hasattr(addon, "get_applications_settings_enum"): - continue + addon = app_addons.latest + if hasattr(addon, "get_applications_settings_enum"): return await addon.get_applications_settings_enum( project_name=project_name, settings_variant=settings_variant,