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/addon.py b/server/addon.py index ee9fe67945..51a987e03e 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 @@ -58,7 +62,17 @@ 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, + get_application_items, + get_tool_items, +) from .actions import ( get_action_manifests, get_dynamic_action_manifests, @@ -342,6 +356,140 @@ 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_addon_for_context( + self, project_name: str | None, variant: str + ) -> BaseServerAddon | 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_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 {} + 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, + ) + 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]: + """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 +739,13 @@ 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) diff --git a/server/utils.py b/server/utils.py new file mode 100644 index 0000000000..a1da855741 --- /dev/null +++ b/server/utils.py @@ -0,0 +1,121 @@ +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