diff --git a/src/middlewared/middlewared/api/v25_04_0/__init__.py b/src/middlewared/middlewared/api/v25_04_0/__init__.py index 692a1410b5f7d..f29fb08f8e28c 100644 --- a/src/middlewared/middlewared/api/v25_04_0/__init__.py +++ b/src/middlewared/middlewared/api/v25_04_0/__init__.py @@ -4,8 +4,10 @@ from .alert import * # noqa from .alertservice import * # noqa from .api_key import * # noqa +from .app import * # noqa from .auth import * # noqa from .boot_environments import * # noqa +from .catalog import * # noqa from .cloud_sync import * # noqa from .common import * # noqa from .core import * # noqa diff --git a/src/middlewared/middlewared/api/v25_04_0/app.py b/src/middlewared/middlewared/api/v25_04_0/app.py new file mode 100644 index 0000000000000..84bc8ad73626a --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/app.py @@ -0,0 +1,31 @@ +from middlewared.api.base import BaseModel, NonEmptyString + +from .catalog import CatalogAppInfo + + +__all__ = [ + 'AppCategoriesArgs', 'AppCategoriesResult', 'AppSimilarArgs', 'AppSimilarResult', 'AppAvailableResponse', +] + + +class AppAvailableResponse(CatalogAppInfo): + catalog: NonEmptyString + installed: bool + train: NonEmptyString + + +class AppCategoriesArgs(BaseModel): + pass + + +class AppCategoriesResult(BaseModel): + result: list[NonEmptyString] + + +class AppSimilarArgs(BaseModel): + app_name: NonEmptyString + train: NonEmptyString + + +class AppSimilarResult(BaseModel): + result: list[AppAvailableResponse] diff --git a/src/middlewared/middlewared/api/v25_04_0/catalog.py b/src/middlewared/middlewared/api/v25_04_0/catalog.py new file mode 100644 index 0000000000000..b8d1c1d4c30ee --- /dev/null +++ b/src/middlewared/middlewared/api/v25_04_0/catalog.py @@ -0,0 +1,123 @@ +from datetime import datetime + +from pydantic import ConfigDict, Field, RootModel + +from middlewared.api.base import BaseModel, ForUpdateMetaclass, NonEmptyString, single_argument_args, LongString + + +__all__ = [ + 'CatalogEntry', 'CatalogUpdateArgs', 'CatalogUpdateResult', 'CatalogTrainsArgs', 'CatalogTrainsResult', + 'CatalogSyncArgs', 'CatalogSyncResult', 'CatalogAppInfo', 'CatalogAppsArgs', 'CatalogAppsResult', + 'CatalogAppDetailsArgs', 'CatalogAppDetailsResult', +] + + +class CatalogEntry(BaseModel): + id: NonEmptyString + label: NonEmptyString = Field(pattern=r'^\w+[\w.-]*$') + preferred_trains: list[NonEmptyString] + + +@single_argument_args('catalog_update') +class CatalogUpdateArgs(BaseModel, metaclass=ForUpdateMetaclass): + preferred_trains: list[NonEmptyString] + + +class CatalogUpdateResult(BaseModel): + result: CatalogEntry + + +class CatalogTrainsArgs(BaseModel): + pass + + +class CatalogTrainsResult(BaseModel): + result: list[NonEmptyString] + + +class CatalogSyncArgs(BaseModel): + pass + + +class CatalogSyncResult(BaseModel): + result: None + + +class Maintainer(BaseModel): + name: str + email: str + url: str | None + + +class CatalogAppInfo(BaseModel): + app_readme: LongString | None + '''HTML content of the app README.''' + categories: list[str] + '''List of categories for the app.''' + description: str + '''Short description of the app.''' + healthy: bool + '''Health status of the app.''' + healthy_error: str | None = None + '''Error if app is not healthy.''' + home: str + '''Homepage URL of the app.''' + location: str + '''Local path to the app's location.''' + latest_version: str | None + '''Latest available app version.''' + latest_app_version: str | None + '''Latest available app version in repository.''' + latest_human_version: str | None + '''Human-readable version of the app.''' + last_update: datetime | None + '''Timestamp of the last update in ISO format.''' + name: str + '''Name of the app.''' + recommended: bool + '''Indicates if the app is recommended.''' + title: str + '''Title of the app.''' + maintainers: list[Maintainer] + '''List of app maintainers.''' + tags: list[str] + '''Tags associated with the app.''' + screenshots: list[str] + '''List of screenshot URLs.''' + sources: list[str] + '''List of source URLs.''' + icon_url: str | None = None + '''URL of the app icon''' + + # We do this because if we change anything in catalog.json, even older releases will + # get this new field and different roles will start breaking due to this + model_config = ConfigDict(extra='allow') + + +@single_argument_args('catalog_apps_options') +class CatalogAppsArgs(BaseModel): + cache: bool = True + cache_only: bool = False + retrieve_all_trains: bool = True + trains: list[NonEmptyString] = Field(default_factory=list) + + +class CatalogTrainInfo(RootModel[dict[str, CatalogAppInfo]]): + pass + + +class CatalogAppsResult(BaseModel): + result: dict[str, CatalogTrainInfo] + + +class CatalogAppVersionDetails(BaseModel): + train: NonEmptyString + + +class CatalogAppDetailsArgs(BaseModel): + app_name: NonEmptyString + app_version_details: CatalogAppVersionDetails + + +class CatalogAppDetailsResult(BaseModel): + result: CatalogAppInfo diff --git a/src/middlewared/middlewared/plugins/catalog/app_version.py b/src/middlewared/middlewared/plugins/catalog/app_version.py index 768a137443703..0545532d67ffc 100644 --- a/src/middlewared/middlewared/plugins/catalog/app_version.py +++ b/src/middlewared/middlewared/plugins/catalog/app_version.py @@ -4,7 +4,8 @@ from catalog_reader.train_utils import get_train_path -from middlewared.schema import accepts, Bool, Dict, List, returns, Str +from middlewared.api import api_method +from middlewared.api.current import CatalogAppDetailsArgs, CatalogAppDetailsResult from middlewared.service import CallError, Service from .apps_util import get_app_details @@ -15,37 +16,7 @@ class CatalogService(Service): class Config: cli_namespace = 'app.catalog' - @accepts( - Str('app_name'), - Dict( - 'app_version_details', - Str('train', required=True), - ), - roles=['CATALOG_READ'], - ) - @returns(Dict( - # TODO: Make sure keys here are mapped appropriately - 'app_details', - Str('name', required=True), - List('categories', items=[Str('category')], required=True), - List('maintainers', required=True), - List('tags', required=True), - List('screenshots', required=True, items=[Str('screenshot')]), - List('sources', required=True, items=[Str('source')]), - Str('app_readme', null=True, required=True), - Str('location', required=True), - Bool('healthy', required=True), - Bool('recommended', required=True), - Str('healthy_error', required=True, null=True), - Str('healthy_error', required=True, null=True), - Dict('versions', required=True, additional_attrs=True), - Str('latest_version', required=True, null=True), - Str('latest_app_version', required=True, null=True), - Str('latest_human_version', required=True, null=True), - Str('last_update', required=True, null=True), - Str('icon_url', required=True, null=True), - Str('home', required=True), - )) + @api_method(CatalogAppDetailsArgs, CatalogAppDetailsResult, roles=['CATALOG_READ']) def get_app_details(self, app_name, options): """ Retrieve information of `app_name` `app_version_details.catalog` catalog app. diff --git a/src/middlewared/middlewared/plugins/catalog/apps.py b/src/middlewared/middlewared/plugins/catalog/apps.py index 4aacb75a69e21..c7b87b8d5a32e 100644 --- a/src/middlewared/middlewared/plugins/catalog/apps.py +++ b/src/middlewared/middlewared/plugins/catalog/apps.py @@ -1,5 +1,9 @@ -from middlewared.schema import accepts, Bool, Datetime, Dict, List, Ref, returns, Str -from middlewared.service import filterable, filterable_returns, Service +from middlewared.api import api_method +from middlewared.api.current import ( + AppCategoriesArgs, AppCategoriesResult, AppAvailableResponse, +) +from middlewared.api.v25_04_0 import AppSimilarArgs, AppSimilarResult +from middlewared.service import filterable_api_method, Service from middlewared.utils import filter_list @@ -8,8 +12,7 @@ class AppService(Service): class Config: cli_namespace = 'app' - @filterable(roles=['CATALOG_READ']) - @filterable_returns(Ref('available_apps')) + @filterable_api_method(item=AppAvailableResponse, roles=['CATALOG_READ']) async def latest(self, filters, options): """ Retrieve latest updated apps. @@ -22,38 +25,7 @@ async def latest(self, filters, options): ), filters, options ) - @filterable(roles=['CATALOG_READ']) - @filterable_returns(Dict( - 'available_apps', - Bool('healthy', required=True), - Bool('installed', required=True), - Bool('recommended', required=True), - Datetime('last_update', required=True), - List('capabilities', required=True), - List('run_as_context', required=True), - List('categories', required=True), - List('maintainers', required=True), - List('tags', required=True), - List('screenshots', required=True, items=[Str('screenshot')]), - List('sources', required=True, items=[Str('source')]), - Str('name', required=True), - Str('title', required=True), - Str('description', required=True), - Str('app_readme', required=True), - Str('location', required=True), - Str('healthy_error', required=True, null=True), - Str('home', required=True), - Str('latest_version', required=True), - Str('latest_app_version', required=True), - Str('latest_human_version', required=True), - Str('icon_url', null=True, required=True), - Str('train', required=True), - Str('catalog', required=True), - register=True, - # We do this because if we change anything in catalog.json, even older releases will - # get this new field and different roles will start breaking due to this - additional_attrs=True, - )) + @filterable_api_method(item=AppAvailableResponse, roles=['CATALOG_READ']) def available(self, filters, options): """ Retrieve all available applications from all configured catalogs. @@ -68,7 +40,7 @@ def available(self, filters, options): ] catalog = self.middleware.call_sync('catalog.config') - for train, train_data in self.middleware.call_sync('catalog.apps').items(): + for train, train_data in self.middleware.call_sync('catalog.apps', {}).items(): if train not in catalog['preferred_trains']: continue @@ -82,16 +54,14 @@ def available(self, filters, options): return filter_list(results, filters, options) - @accepts(roles=['CATALOG_READ']) - @returns(List(items=[Str('category')])) + @api_method(AppCategoriesArgs, AppCategoriesResult, roles=['CATALOG_READ']) async def categories(self): """ Retrieve list of valid categories which have associated applications. """ return sorted(list(await self.middleware.call('catalog.retrieve_mapped_categories'))) - @accepts(Str('app_name'), Str('train'), roles=['CATALOG_READ']) - @returns(List(items=[Ref('available_apps')])) + @api_method(AppSimilarArgs, AppSimilarResult, roles=['CATALOG_READ']) def similar(self, app_name, train): """ Retrieve applications which are similar to `app_name`. diff --git a/src/middlewared/middlewared/plugins/catalog/apps_details.py b/src/middlewared/middlewared/plugins/catalog/apps_details.py index d69cc7cb57aca..2cacd1e46378c 100644 --- a/src/middlewared/middlewared/plugins/catalog/apps_details.py +++ b/src/middlewared/middlewared/plugins/catalog/apps_details.py @@ -11,7 +11,8 @@ from datetime import datetime from jsonschema import validate as json_schema_validate, ValidationError as JsonValidationError -from middlewared.schema import accepts, Bool, Dict, List, returns, Str +from middlewared.api import api_method +from middlewared.api.current import CatalogAppsArgs, CatalogAppsResult from middlewared.service import private, Service from .apps_util import get_app_version_details @@ -45,47 +46,7 @@ def train_to_apps_version_mapping(self): def cached(self, label): return self.middleware.call_sync('cache.has_key', get_cache_key(label)) - @accepts( - Dict( - 'options', - Bool('cache', default=True), - Bool('cache_only', default=False), - Bool('retrieve_all_trains', default=True), - List('trains', items=[Str('train_name')]), - ), - roles=['CATALOG_READ'] - ) - @returns(Dict( - 'trains', - additional_attrs=True, - example={ - 'stable': { - 'plex': { - 'app_readme': '