From 670d502a7c46a4a0fe59466ed40b2234304da4d9 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Fri, 20 Dec 2024 17:07:36 +0200 Subject: [PATCH] make apispec dependency optional --- .pylintrc | 2 +- CONTRIBUTING.md | 29 ++-- docs/ahriman.web.apispec.rst | 29 ++++ docs/ahriman.web.rst | 9 +- package/archlinux/PKGBUILD | 3 +- .../ahriman/templates/build-status.jinja2 | 4 +- src/ahriman/web/apispec/__init__.py | 32 ++++ src/ahriman/web/apispec/decorators.py | 148 ++++++++++++++++ .../web/{apispec.py => apispec/info.py} | 10 +- src/ahriman/web/schemas/aur_package_schema.py | 2 +- src/ahriman/web/schemas/auth_schema.py | 2 +- .../web/schemas/build_options_schema.py | 2 +- src/ahriman/web/schemas/changes_schema.py | 2 +- src/ahriman/web/schemas/counters_schema.py | 2 +- .../web/schemas/dependencies_schema.py | 2 +- src/ahriman/web/schemas/error_schema.py | 2 +- src/ahriman/web/schemas/event_schema.py | 3 +- .../web/schemas/event_search_schema.py | 3 +- src/ahriman/web/schemas/file_schema.py | 2 +- src/ahriman/web/schemas/info_schema.py | 3 +- .../web/schemas/internal_status_schema.py | 3 +- src/ahriman/web/schemas/log_schema.py | 2 +- src/ahriman/web/schemas/login_schema.py | 2 +- src/ahriman/web/schemas/logs_schema.py | 3 +- src/ahriman/web/schemas/oauth2_schema.py | 2 +- .../web/schemas/package_name_schema.py | 2 +- .../web/schemas/package_names_schema.py | 3 +- .../web/schemas/package_patch_schema.py | 3 +- .../web/schemas/package_properties_schema.py | 2 +- src/ahriman/web/schemas/package_schema.py | 3 +- .../web/schemas/package_status_schema.py | 3 +- .../web/schemas/package_version_schema.py | 3 +- src/ahriman/web/schemas/pagination_schema.py | 3 +- src/ahriman/web/schemas/patch_name_schema.py | 3 +- src/ahriman/web/schemas/patch_schema.py | 2 +- src/ahriman/web/schemas/pgp_key_id_schema.py | 2 +- src/ahriman/web/schemas/pgp_key_schema.py | 2 +- src/ahriman/web/schemas/process_id_schema.py | 2 +- src/ahriman/web/schemas/process_schema.py | 2 +- src/ahriman/web/schemas/remote_schema.py | 3 +- .../web/schemas/repository_id_schema.py | 2 +- src/ahriman/web/schemas/search_schema.py | 2 +- src/ahriman/web/schemas/status_schema.py | 3 +- .../web/schemas/update_flags_schema.py | 3 +- .../web/schemas/versioned_log_schema.py | 3 +- src/ahriman/web/schemas/worker_schema.py | 2 +- src/ahriman/web/views/api/docs.py | 18 ++ src/ahriman/web/views/api/swagger.py | 18 ++ src/ahriman/web/views/index.py | 3 + src/ahriman/web/views/v1/auditlog/events.py | 36 ++-- .../web/views/v1/distributed/workers.py | 43 ++--- src/ahriman/web/views/v1/packages/changes.py | 44 ++--- .../web/views/v1/packages/dependencies.py | 44 ++--- src/ahriman/web/views/v1/packages/logs.py | 61 +++---- src/ahriman/web/views/v1/packages/package.py | 63 +++---- src/ahriman/web/views/v1/packages/packages.py | 39 ++--- src/ahriman/web/views/v1/packages/patch.py | 34 ++-- src/ahriman/web/views/v1/packages/patches.py | 36 ++-- src/ahriman/web/views/v1/service/add.py | 25 +-- src/ahriman/web/views/v1/service/pgp.py | 39 ++--- src/ahriman/web/views/v1/service/process.py | 21 +-- src/ahriman/web/views/v1/service/rebuild.py | 25 +-- src/ahriman/web/views/v1/service/remove.py | 25 +-- src/ahriman/web/views/v1/service/request.py | 25 +-- src/ahriman/web/views/v1/service/search.py | 23 +-- src/ahriman/web/views/v1/service/update.py | 25 +-- src/ahriman/web/views/v1/service/upload.py | 47 ++--- src/ahriman/web/views/v1/status/info.py | 17 +- .../web/views/v1/status/repositories.py | 17 +- src/ahriman/web/views/v1/status/status.py | 39 ++--- src/ahriman/web/views/v1/user/login.py | 31 ++-- src/ahriman/web/views/v1/user/logout.py | 15 +- src/ahriman/web/views/v2/packages/logs.py | 25 +-- src/ahriman/web/web.py | 2 +- tests/ahriman/web/apispec/test_apispec.py | 24 +++ tests/ahriman/web/apispec/test_decorators.py | 161 ++++++++++++++++++ .../{test_apispec.py => apispec/test_info.py} | 13 +- .../web/views/api/test_view_api_docs.py | 24 +++ .../web/views/api/test_view_api_swagger.py | 23 +++ .../v1/service/test_view_v1_service_upload.py | 30 ++-- 80 files changed, 843 insertions(+), 628 deletions(-) create mode 100644 docs/ahriman.web.apispec.rst create mode 100644 src/ahriman/web/apispec/__init__.py create mode 100644 src/ahriman/web/apispec/decorators.py rename src/ahriman/web/{apispec.py => apispec/info.py} (94%) create mode 100644 tests/ahriman/web/apispec/test_apispec.py create mode 100644 tests/ahriman/web/apispec/test_decorators.py rename tests/ahriman/web/{test_apispec.py => apispec/test_info.py} (78%) diff --git a/.pylintrc b/.pylintrc index 42df9005..371b21f3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -305,7 +305,7 @@ max-branches=12 max-locals=15 # Maximum number of parents for a class (see R0901). -max-parents=7 +max-parents=15 # Maximum number of public methods for a class (see R0904). max-public-methods=20 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07eb92b7..d970a96e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -175,11 +175,10 @@ Again, the most checks can be performed by `tox` command, though some additional * Web API methods must be documented by using `aiohttp_apispec` library. The schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments): ```python - import aiohttp_apispec - from marshmallow import Schema, fields - - from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PaginationSchema + + from ahriman.web.apispec.decorators import apidocs + from ahriman.web.schemas import PackageNameSchema, PaginationSchema from ahriman.web.views.base import BaseView @@ -198,25 +197,17 @@ Again, the most checks can be performed by `tox` command, though some additional POST_PERMISSION = ... ROUTES = ... - @aiohttp_apispec.docs( + @apidocs( tags=["Tag"], summary="Do foo", description="Extended description of the method which does foo", - responses={ - 200: {"description": "Success response", "schema": ResponseSchema}, - 204: {"description": "Success response"}, # example without json schema response - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, # exception raised by this method - 401: {"description": "Authorization required", "schema": ErrorSchema}, # should be always presented - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, # should be always presented - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, # include if BaseView.service() method is called - 500: {"description": "Internal server error", "schema": ErrorSchema}, # should be always presented - }, - security=[{"token": [POST_PERMISSION]}], + error_400_enabled=True, # exception raised by this method + error_404_description="Repository is unknown", + schema=ResponseSchema, # leave empty if no responses here + match_schema=PackageNameSchema, + query_schema=PaginationSchema, + body_schema=RequestSchema(many=True), ) - @aiohttp_apispec.cookies_schema(AuthSchema) # should be always presented - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(PaginationSchema) - @aiohttp_apispec.json_schema(RequestSchema(many=True)) async def post(self) -> None: ... ``` diff --git a/docs/ahriman.web.apispec.rst b/docs/ahriman.web.apispec.rst new file mode 100644 index 00000000..5b2e205d --- /dev/null +++ b/docs/ahriman.web.apispec.rst @@ -0,0 +1,29 @@ +ahriman.web.apispec package +=========================== + +Submodules +---------- + +ahriman.web.apispec.decorators module +------------------------------------- + +.. automodule:: ahriman.web.apispec.decorators + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.apispec.info module +------------------------------- + +.. automodule:: ahriman.web.apispec.info + :members: + :no-undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ahriman.web.apispec + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.rst b/docs/ahriman.web.rst index 59a59712..c175f4ac 100644 --- a/docs/ahriman.web.rst +++ b/docs/ahriman.web.rst @@ -7,6 +7,7 @@ Subpackages .. toctree:: :maxdepth: 4 + ahriman.web.apispec ahriman.web.middlewares ahriman.web.schemas ahriman.web.views @@ -14,14 +15,6 @@ Subpackages Submodules ---------- -ahriman.web.apispec module --------------------------- - -.. automodule:: ahriman.web.apispec - :members: - :no-undoc-members: - :show-inheritance: - ahriman.web.cors module ----------------------- diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 446bd95a..9f10fcb0 100644 --- a/package/archlinux/PKGBUILD +++ b/package/archlinux/PKGBUILD @@ -72,8 +72,9 @@ package_ahriman-triggers() { package_ahriman-web() { pkgname='ahriman-web' pkgdesc="ArcH linux ReposItory MANager, web server" - depends=("$pkgbase-core=$pkgver" 'python-aiohttp-apispec>=3.0.0' 'python-aiohttp-cors' 'python-aiohttp-jinja2') + depends=("$pkgbase-core=$pkgver" 'python-aiohttp-cors' 'python-aiohttp-jinja2') optdepends=('python-aioauth-client: OAuth2 authorization support' + 'python-aiohttp-apispec>=3.0.0: autogenerated API documentation' 'python-aiohttp-security: authorization support' 'python-aiohttp-session: authorization support' 'python-cryptography: authorization support') diff --git a/package/share/ahriman/templates/build-status.jinja2 b/package/share/ahriman/templates/build-status.jinja2 index 93f321db..6b90fd0d 100644 --- a/package/share/ahriman/templates/build-status.jinja2 +++ b/package/share/ahriman/templates/build-status.jinja2 @@ -119,7 +119,9 @@
  • ahriman
  • releases
  • report a bug
  • -
  • api
  • + {% if docs_enabled %} +
  • api
  • + {% endif %} {% if index_url is not none %} diff --git a/src/ahriman/web/apispec/__init__.py b/src/ahriman/web/apispec/__init__.py new file mode 100644 index 00000000..c3bfeaad --- /dev/null +++ b/src/ahriman/web/apispec/__init__.py @@ -0,0 +1,32 @@ +# +# Copyright (c) 2021-2024 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +try: + import aiohttp_apispec # type: ignore[import-untyped] + + from marshmallow import Schema, fields +except ImportError: + from unittest.mock import Mock + + Schema = Mock # type: ignore[misc] + aiohttp_apispec = None + fields = Mock() + + +__all__ = ["Schema", "aiohttp_apispec", "fields"] diff --git a/src/ahriman/web/apispec/decorators.py b/src/ahriman/web/apispec/decorators.py new file mode 100644 index 00000000..45077e7d --- /dev/null +++ b/src/ahriman/web/apispec/decorators.py @@ -0,0 +1,148 @@ +# +# Copyright (c) 2021-2024 ahriman team. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +from aiohttp.web import HTTPException +from typing import Any, Callable + +from ahriman.models.user_access import UserAccess +from ahriman.web.apispec import Schema, aiohttp_apispec +from ahriman.web.schemas import AuthSchema, ErrorSchema + + +__all__ = ["apidocs"] + + +def _response_schema(response: Schema | type[Schema] | None, response_code: type[HTTPException] | None = None, + error_400_enabled: bool = False, error_403_enabled: bool = True, + error_404_description: str | None = None) -> dict[int, Any]: + """ + render response schema specification + + Args: + response(Schema | type[Schema] | None): response schema type, set ``None`` for empty responses + response_code(type[HTTPException] | None, optional): code for the success response. If none set it will be + defined automatically (Default value = None) + error_400_enabled(bool, optional): include response for 404 codes (Default value = False) + error_403_enabled(bool, optional): include response for 403 codes (Default value = False) + error_404_description(str | None, optional): description for 404 codes if available (Default value = None) + + Returns: + dict[int, Any]: response schema in apispec format + """ + schema = { + 401: {"description": "Authorization required", "schema": ErrorSchema}, + 500: {"description": "Internal server error", "schema": ErrorSchema}, + } + + match response_code: + case None if response is None: + code = 204 + case None: + code = 200 + case exception: + code = exception.status_code + schema[code] = {"description": "Success response"} + if response is not None: + schema[code]["schema"] = response + + if error_400_enabled: + schema[400] = {"description": "Bad request", "schema": ErrorSchema} + + if error_403_enabled: + schema[403] = {"description": "Access is forbidden", "schema": ErrorSchema} + + if error_404_description is not None: + schema[404] = {"description": error_404_description, "schema": ErrorSchema} + + return schema + + +def apidocs(*, + tags: list[str], + summary: str, + description: str, + permission: UserAccess, + response_code: type[HTTPException] | None = None, + error_400_enabled: bool = False, + error_404_description: str | None = None, + schema: Schema | type[Schema] | None = None, + match_schema: Schema | type[Schema] | None = None, + query_schema: Schema | type[Schema] | None = None, + body_schema: Schema | type[Schema] | None = None, + body_location: str = "json", + ) -> Callable[..., Any]: + """ + wrapper around :mod:`aiohttp_apispec` to decorate HTTP methods + + Args: + tags(list[str]): list of tags for the endpoint + summary(str): summary for the endpoint + description(str): long description for the endpoint + permission(UserAccess): permission to access endpoint + response_code(type[HTTPException] | None, optional): code for the success response. If none set it will be + defined automatically (Default value = None) + error_400_enabled(bool, optional): include response for 404 codes (Default value = False) + error_404_description(str | None, optional): description for 404 codes if available (Default value = None) + schema(Schema | type[Schema] | None): response schema type, set ``None`` for empty responses + (Default value = None) + match_schema(Schema | type[Schema] | None): schema for uri matcher if used (Default value = None) + query_schema(Schema | type[Schema] | None): query string schema type, set ``None`` if not applicable + (Default value = None) + body_schema(Schema | type[Schema] | None): body schema type, set ``None`` if not applicable + (Default value = None) + body_location(str, optional): body location name (Default value = "json") + + Returns: + Callable[..., Any]: decorated function + """ + authorization_required = permission != UserAccess.Unauthorized + + def wrapper(handler: Callable[..., Any]) -> Callable[..., Any]: + if aiohttp_apispec is None: + return handler # apispec is disabled + + responses = _response_schema( + response=schema, + response_code=response_code, + error_400_enabled=error_400_enabled, + error_403_enabled=authorization_required, + error_404_description=error_404_description, + ) + handler = aiohttp_apispec.docs( + tags=tags, + summary=summary, + description=description, + responses=responses, + security=[{"token": [permission]}], + )(handler) + + # request schemas + if authorization_required: + handler = aiohttp_apispec.cookies_schema(AuthSchema)(handler) + if match_schema is not None: + handler = aiohttp_apispec.match_info_schema(match_schema)(handler) + if query_schema is not None: + handler = aiohttp_apispec.querystring_schema(query_schema)(handler) + if body_schema is not None: + handler = aiohttp_apispec.request_schema( + body_schema, locations=[body_location], put_into=body_location)(handler) + + return handler + + return wrapper diff --git a/src/ahriman/web/apispec.py b/src/ahriman/web/apispec/info.py similarity index 94% rename from src/ahriman/web/apispec.py rename to src/ahriman/web/apispec/info.py index 5c825772..67756c2c 100644 --- a/src/ahriman/web/apispec.py +++ b/src/ahriman/web/apispec/info.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import Application from typing import Any from ahriman import __version__ +from ahriman.web.apispec import aiohttp_apispec from ahriman.web.keys import ConfigurationKey @@ -101,7 +100,7 @@ def _servers(application: Application) -> list[dict[str, Any]]: }] -def setup_apispec(application: Application) -> aiohttp_apispec.AiohttpApiSpec: +def setup_apispec(application: Application) -> Any: """ setup swagger api specification @@ -109,8 +108,11 @@ def setup_apispec(application: Application) -> aiohttp_apispec.AiohttpApiSpec: application(Application): web application instance Returns: - aiohttp_apispec.AiohttpApiSpec: created specification instance + Any: created specification instance if module is available """ + if aiohttp_apispec is None: + return None + return aiohttp_apispec.setup_aiohttp_apispec( application, url="/api-docs/swagger.json", diff --git a/src/ahriman/web/schemas/aur_package_schema.py b/src/ahriman/web/schemas/aur_package_schema.py index e8c06ef3..11b43861 100644 --- a/src/ahriman/web/schemas/aur_package_schema.py +++ b/src/ahriman/web/schemas/aur_package_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class AURPackageSchema(Schema): diff --git a/src/ahriman/web/schemas/auth_schema.py b/src/ahriman/web/schemas/auth_schema.py index f0c20978..64ad9dd6 100644 --- a/src/ahriman/web/schemas/auth_schema.py +++ b/src/ahriman/web/schemas/auth_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class AuthSchema(Schema): diff --git a/src/ahriman/web/schemas/build_options_schema.py b/src/ahriman/web/schemas/build_options_schema.py index 41265212..10df2bf5 100644 --- a/src/ahriman/web/schemas/build_options_schema.py +++ b/src/ahriman/web/schemas/build_options_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class BuildOptionsSchema(Schema): diff --git a/src/ahriman/web/schemas/changes_schema.py b/src/ahriman/web/schemas/changes_schema.py index 0cf2fad8..ab11c57a 100644 --- a/src/ahriman/web/schemas/changes_schema.py +++ b/src/ahriman/web/schemas/changes_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class ChangesSchema(Schema): diff --git a/src/ahriman/web/schemas/counters_schema.py b/src/ahriman/web/schemas/counters_schema.py index 6e3b3edc..85dccaf5 100644 --- a/src/ahriman/web/schemas/counters_schema.py +++ b/src/ahriman/web/schemas/counters_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class CountersSchema(Schema): diff --git a/src/ahriman/web/schemas/dependencies_schema.py b/src/ahriman/web/schemas/dependencies_schema.py index 0c8d98ac..02135d41 100644 --- a/src/ahriman/web/schemas/dependencies_schema.py +++ b/src/ahriman/web/schemas/dependencies_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class DependenciesSchema(Schema): diff --git a/src/ahriman/web/schemas/error_schema.py b/src/ahriman/web/schemas/error_schema.py index feac542d..d2d9b100 100644 --- a/src/ahriman/web/schemas/error_schema.py +++ b/src/ahriman/web/schemas/error_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class ErrorSchema(Schema): diff --git a/src/ahriman/web/schemas/event_schema.py b/src/ahriman/web/schemas/event_schema.py index d9647d0c..a95cf146 100644 --- a/src/ahriman/web/schemas/event_schema.py +++ b/src/ahriman/web/schemas/event_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - from ahriman.models.event import EventType +from ahriman.web.apispec import Schema, fields class EventSchema(Schema): diff --git a/src/ahriman/web/schemas/event_search_schema.py b/src/ahriman/web/schemas/event_search_schema.py index f35364b9..374480b3 100644 --- a/src/ahriman/web/schemas/event_search_schema.py +++ b/src/ahriman/web/schemas/event_search_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - from ahriman.models.event import EventType +from ahriman.web.apispec import fields from ahriman.web.schemas.pagination_schema import PaginationSchema diff --git a/src/ahriman/web/schemas/file_schema.py b/src/ahriman/web/schemas/file_schema.py index a5bf15cd..31159526 100644 --- a/src/ahriman/web/schemas/file_schema.py +++ b/src/ahriman/web/schemas/file_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class FileSchema(Schema): diff --git a/src/ahriman/web/schemas/info_schema.py b/src/ahriman/web/schemas/info_schema.py index 9fbf4bfb..0b302037 100644 --- a/src/ahriman/web/schemas/info_schema.py +++ b/src/ahriman/web/schemas/info_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - from ahriman import __version__ +from ahriman.web.apispec import Schema, fields from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema diff --git a/src/ahriman/web/schemas/internal_status_schema.py b/src/ahriman/web/schemas/internal_status_schema.py index 50080416..c2814fc5 100644 --- a/src/ahriman/web/schemas/internal_status_schema.py +++ b/src/ahriman/web/schemas/internal_status_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - from ahriman import __version__ +from ahriman.web.apispec import fields from ahriman.web.schemas.counters_schema import CountersSchema from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema from ahriman.web.schemas.status_schema import StatusSchema diff --git a/src/ahriman/web/schemas/log_schema.py b/src/ahriman/web/schemas/log_schema.py index 4252116e..a557bae2 100644 --- a/src/ahriman/web/schemas/log_schema.py +++ b/src/ahriman/web/schemas/log_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class LogSchema(Schema): diff --git a/src/ahriman/web/schemas/login_schema.py b/src/ahriman/web/schemas/login_schema.py index 66752aff..1d96fe2b 100644 --- a/src/ahriman/web/schemas/login_schema.py +++ b/src/ahriman/web/schemas/login_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class LoginSchema(Schema): diff --git a/src/ahriman/web/schemas/logs_schema.py b/src/ahriman/web/schemas/logs_schema.py index c32dee26..33d5da27 100644 --- a/src/ahriman/web/schemas/logs_schema.py +++ b/src/ahriman/web/schemas/logs_schema.py @@ -17,8 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - +from ahriman.web.apispec import Schema, fields from ahriman.web.schemas.status_schema import StatusSchema diff --git a/src/ahriman/web/schemas/oauth2_schema.py b/src/ahriman/web/schemas/oauth2_schema.py index 5ddbef39..e9fffa11 100644 --- a/src/ahriman/web/schemas/oauth2_schema.py +++ b/src/ahriman/web/schemas/oauth2_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class OAuth2Schema(Schema): diff --git a/src/ahriman/web/schemas/package_name_schema.py b/src/ahriman/web/schemas/package_name_schema.py index ee43e39d..4fd571f8 100644 --- a/src/ahriman/web/schemas/package_name_schema.py +++ b/src/ahriman/web/schemas/package_name_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class PackageNameSchema(Schema): diff --git a/src/ahriman/web/schemas/package_names_schema.py b/src/ahriman/web/schemas/package_names_schema.py index b8dcf881..d571e56d 100644 --- a/src/ahriman/web/schemas/package_names_schema.py +++ b/src/ahriman/web/schemas/package_names_schema.py @@ -17,8 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - +from ahriman.web.apispec import fields from ahriman.web.schemas.build_options_schema import BuildOptionsSchema diff --git a/src/ahriman/web/schemas/package_patch_schema.py b/src/ahriman/web/schemas/package_patch_schema.py index d72835df..79cf2857 100644 --- a/src/ahriman/web/schemas/package_patch_schema.py +++ b/src/ahriman/web/schemas/package_patch_schema.py @@ -17,8 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - +from ahriman.web.apispec import fields from ahriman.web.schemas.package_names_schema import PackageNamesSchema from ahriman.web.schemas.patch_schema import PatchSchema diff --git a/src/ahriman/web/schemas/package_properties_schema.py b/src/ahriman/web/schemas/package_properties_schema.py index 1e30cbce..c7c11399 100644 --- a/src/ahriman/web/schemas/package_properties_schema.py +++ b/src/ahriman/web/schemas/package_properties_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class PackagePropertiesSchema(Schema): diff --git a/src/ahriman/web/schemas/package_schema.py b/src/ahriman/web/schemas/package_schema.py index 20fa5006..55ead9a8 100644 --- a/src/ahriman/web/schemas/package_schema.py +++ b/src/ahriman/web/schemas/package_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - from ahriman import __version__ +from ahriman.web.apispec import Schema, fields from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema from ahriman.web.schemas.remote_schema import RemoteSchema diff --git a/src/ahriman/web/schemas/package_status_schema.py b/src/ahriman/web/schemas/package_status_schema.py index b5a97490..52f659b2 100644 --- a/src/ahriman/web/schemas/package_status_schema.py +++ b/src/ahriman/web/schemas/package_status_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - from ahriman.models.build_status import BuildStatusEnum +from ahriman.web.apispec import Schema, fields from ahriman.web.schemas.package_schema import PackageSchema from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema from ahriman.web.schemas.status_schema import StatusSchema diff --git a/src/ahriman/web/schemas/package_version_schema.py b/src/ahriman/web/schemas/package_version_schema.py index 8ec967db..8ef983a0 100644 --- a/src/ahriman/web/schemas/package_version_schema.py +++ b/src/ahriman/web/schemas/package_version_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - from ahriman import __version__ +from ahriman.web.apispec import fields from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema diff --git a/src/ahriman/web/schemas/pagination_schema.py b/src/ahriman/web/schemas/pagination_schema.py index a7804606..999aa374 100644 --- a/src/ahriman/web/schemas/pagination_schema.py +++ b/src/ahriman/web/schemas/pagination_schema.py @@ -17,8 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - +from ahriman.web.apispec import fields from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema diff --git a/src/ahriman/web/schemas/patch_name_schema.py b/src/ahriman/web/schemas/patch_name_schema.py index 16529839..b9a969aa 100644 --- a/src/ahriman/web/schemas/patch_name_schema.py +++ b/src/ahriman/web/schemas/patch_name_schema.py @@ -17,8 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - +from ahriman.web.apispec import fields from ahriman.web.schemas.package_name_schema import PackageNameSchema diff --git a/src/ahriman/web/schemas/patch_schema.py b/src/ahriman/web/schemas/patch_schema.py index 56032128..8e441733 100644 --- a/src/ahriman/web/schemas/patch_schema.py +++ b/src/ahriman/web/schemas/patch_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class PatchSchema(Schema): diff --git a/src/ahriman/web/schemas/pgp_key_id_schema.py b/src/ahriman/web/schemas/pgp_key_id_schema.py index fd9f5660..aaf9b0ea 100644 --- a/src/ahriman/web/schemas/pgp_key_id_schema.py +++ b/src/ahriman/web/schemas/pgp_key_id_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class PGPKeyIdSchema(Schema): diff --git a/src/ahriman/web/schemas/pgp_key_schema.py b/src/ahriman/web/schemas/pgp_key_schema.py index d0e35e59..24bcf3d1 100644 --- a/src/ahriman/web/schemas/pgp_key_schema.py +++ b/src/ahriman/web/schemas/pgp_key_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class PGPKeySchema(Schema): diff --git a/src/ahriman/web/schemas/process_id_schema.py b/src/ahriman/web/schemas/process_id_schema.py index ef5f6c27..0861676a 100644 --- a/src/ahriman/web/schemas/process_id_schema.py +++ b/src/ahriman/web/schemas/process_id_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class ProcessIdSchema(Schema): diff --git a/src/ahriman/web/schemas/process_schema.py b/src/ahriman/web/schemas/process_schema.py index 3f7a52e4..c0099eb4 100644 --- a/src/ahriman/web/schemas/process_schema.py +++ b/src/ahriman/web/schemas/process_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class ProcessSchema(Schema): diff --git a/src/ahriman/web/schemas/remote_schema.py b/src/ahriman/web/schemas/remote_schema.py index 92be83ba..0e46f4cf 100644 --- a/src/ahriman/web/schemas/remote_schema.py +++ b/src/ahriman/web/schemas/remote_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - from ahriman.models.package_source import PackageSource +from ahriman.web.apispec import Schema, fields class RemoteSchema(Schema): diff --git a/src/ahriman/web/schemas/repository_id_schema.py b/src/ahriman/web/schemas/repository_id_schema.py index a078c7ba..82b3ab9d 100644 --- a/src/ahriman/web/schemas/repository_id_schema.py +++ b/src/ahriman/web/schemas/repository_id_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class RepositoryIdSchema(Schema): diff --git a/src/ahriman/web/schemas/search_schema.py b/src/ahriman/web/schemas/search_schema.py index 646024a2..34645c98 100644 --- a/src/ahriman/web/schemas/search_schema.py +++ b/src/ahriman/web/schemas/search_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class SearchSchema(Schema): diff --git a/src/ahriman/web/schemas/status_schema.py b/src/ahriman/web/schemas/status_schema.py index e7857eda..d7b4f350 100644 --- a/src/ahriman/web/schemas/status_schema.py +++ b/src/ahriman/web/schemas/status_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields - from ahriman.models.build_status import BuildStatusEnum +from ahriman.web.apispec import Schema, fields class StatusSchema(Schema): diff --git a/src/ahriman/web/schemas/update_flags_schema.py b/src/ahriman/web/schemas/update_flags_schema.py index 3cf071bc..ef6434ab 100644 --- a/src/ahriman/web/schemas/update_flags_schema.py +++ b/src/ahriman/web/schemas/update_flags_schema.py @@ -17,8 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - +from ahriman.web.apispec import fields from ahriman.web.schemas.build_options_schema import BuildOptionsSchema diff --git a/src/ahriman/web/schemas/versioned_log_schema.py b/src/ahriman/web/schemas/versioned_log_schema.py index c7b18f30..e3dd0ba5 100644 --- a/src/ahriman/web/schemas/versioned_log_schema.py +++ b/src/ahriman/web/schemas/versioned_log_schema.py @@ -17,9 +17,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import fields - from ahriman import __version__ +from ahriman.web.apispec import fields from ahriman.web.schemas.log_schema import LogSchema from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema diff --git a/src/ahriman/web/schemas/worker_schema.py b/src/ahriman/web/schemas/worker_schema.py index 87afc769..6822ad67 100644 --- a/src/ahriman/web/schemas/worker_schema.py +++ b/src/ahriman/web/schemas/worker_schema.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -from marshmallow import Schema, fields +from ahriman.web.apispec import Schema, fields class WorkerSchema(Schema): diff --git a/src/ahriman/web/views/api/docs.py b/src/ahriman/web/views/api/docs.py index 66922dfe..b338e480 100644 --- a/src/ahriman/web/views/api/docs.py +++ b/src/ahriman/web/views/api/docs.py @@ -21,7 +21,9 @@ import aiohttp_jinja2 from typing import Any +from ahriman.core.configuration import Configuration from ahriman.models.user_access import UserAccess +from ahriman.web.apispec import aiohttp_apispec from ahriman.web.views.base import BaseView @@ -36,6 +38,22 @@ class DocsView(BaseView): GET_PERMISSION = UserAccess.Unauthorized ROUTES = ["/api-docs"] + @classmethod + def routes(cls, configuration: Configuration) -> list[str]: + """ + extract routes list for the view + + Args: + configuration(Configuration): configuration instance + + Returns: + list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set + and returns empty list otherwise + """ + if aiohttp_apispec is None: + return [] + return cls.ROUTES + @aiohttp_jinja2.template("api.jinja2") async def get(self) -> dict[str, Any]: """ diff --git a/src/ahriman/web/views/api/swagger.py b/src/ahriman/web/views/api/swagger.py index ae16c57c..694daa1a 100644 --- a/src/ahriman/web/views/api/swagger.py +++ b/src/ahriman/web/views/api/swagger.py @@ -20,8 +20,10 @@ from aiohttp.web import Response, json_response from collections.abc import Callable +from ahriman.core.configuration import Configuration from ahriman.core.utils import partition from ahriman.models.user_access import UserAccess +from ahriman.web.apispec import aiohttp_apispec from ahriman.web.views.base import BaseView @@ -36,6 +38,22 @@ class SwaggerView(BaseView): GET_PERMISSION = UserAccess.Unauthorized ROUTES = ["/api-docs/swagger.json"] + @classmethod + def routes(cls, configuration: Configuration) -> list[str]: + """ + extract routes list for the view + + Args: + configuration(Configuration): configuration instance + + Returns: + list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set + and returns empty list otherwise + """ + if aiohttp_apispec is None: + return [] + return cls.ROUTES + async def get(self) -> Response: """ get api specification diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index 8a02a518..0445ba52 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -23,6 +23,7 @@ from typing import Any from ahriman.core.auth.helpers import authorized_userid from ahriman.models.user_access import UserAccess +from ahriman.web.apispec import aiohttp_apispec from ahriman.web.views.base import BaseView @@ -36,6 +37,7 @@ class IndexView(BaseView): * control - HTML to insert for login control, HTML string, required * enabled - whether authorization is enabled by configuration or not, boolean, required * username - authenticated username if any, string, null means not authenticated + * docs_enabled - indicates if api docs is enabled, boolean, required * index_url - url to the repository index, string, optional * repositories - list of repositories unique identifiers, required * id - unique repository identifier, string, required @@ -66,6 +68,7 @@ class IndexView(BaseView): return { "auth": auth, + "docs_enabled": aiohttp_apispec is not None, "index_url": self.configuration.get("web", "index_url", fallback=None), "repositories": [ { diff --git a/src/ahriman/web/views/v1/auditlog/events.py b/src/ahriman/web/views/v1/auditlog/events.py index f696f643..a173ef6c 100644 --- a/src/ahriman/web/views/v1/auditlog/events.py +++ b/src/ahriman/web/views/v1/auditlog/events.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from ahriman.models.event import Event from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, EventSchema, EventSearchSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import EventSchema, EventSearchSchema from ahriman.web.views.base import BaseView @@ -39,21 +38,15 @@ class EventsView(BaseView): GET_PERMISSION = POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/events"] - @aiohttp_apispec.docs( + @apidocs( tags=["Audit log"], summary="Get events", description="Retrieve events from audit log", - responses={ - 200: {"description": "Success response", "schema": EventSchema(many=True)}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_400_enabled=True, + schema=EventSchema(many=True), + query_schema=EventSearchSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(EventSearchSchema) async def get(self) -> Response: """ get events list @@ -78,21 +71,14 @@ class EventsView(BaseView): return json_response(response) - @aiohttp_apispec.docs( + @apidocs( tags=["Audit log"], summary="Create event", description="Add new event to the audit log", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + body_schema=EventSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.json_schema(EventSchema) async def post(self) -> None: """ add new audit log event diff --git a/src/ahriman/web/views/v1/distributed/workers.py b/src/ahriman/web/views/v1/distributed/workers.py index bb0bbd23..548c2391 100644 --- a/src/ahriman/web/views/v1/distributed/workers.py +++ b/src/ahriman/web/views/v1/distributed/workers.py @@ -17,14 +17,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from collections.abc import Callable from ahriman.models.user_access import UserAccess from ahriman.models.worker import Worker -from ahriman.web.schemas import AuthSchema, ErrorSchema, WorkerSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import WorkerSchema from ahriman.web.views.base import BaseView @@ -41,19 +40,12 @@ class WorkersView(BaseView): DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/distributed"] - @aiohttp_apispec.docs( + @apidocs( tags=["Distributed"], summary="Unregister all workers", description="Unregister and remove all known workers from the service", - responses={ - 204: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [DELETE_PERMISSION]}], + permission=DELETE_PERMISSION, ) - @aiohttp_apispec.cookies_schema(AuthSchema) async def delete(self) -> None: """ unregister worker @@ -65,19 +57,13 @@ class WorkersView(BaseView): raise HTTPNoContent - @aiohttp_apispec.docs( + @apidocs( tags=["Distributed"], summary="Get workers", description="Retrieve registered workers", - responses={ - 200: {"description": "Success response", "schema": WorkerSchema(many=True)}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + schema=WorkerSchema(many=True), ) - @aiohttp_apispec.cookies_schema(AuthSchema) async def get(self) -> Response: """ get workers list @@ -92,21 +78,14 @@ class WorkersView(BaseView): return json_response(response) - @aiohttp_apispec.docs( + @apidocs( tags=["Distributed"], summary="Register worker", description="Register or update remote worker", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + body_schema=WorkerSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.json_schema(WorkerSchema) async def post(self) -> None: """ register remote worker diff --git a/src/ahriman/web/views/v1/packages/changes.py b/src/ahriman/web/views/v1/packages/changes.py index 6a7765d7..ef3e359b 100644 --- a/src/ahriman/web/views/v1/packages/changes.py +++ b/src/ahriman/web/views/v1/packages/changes.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from ahriman.models.changes import Changes from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ChangesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import ChangesSchema, PackageNameSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -41,22 +40,16 @@ class ChangesView(StatusViewGuard, BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/packages/{package}/changes"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get package changes", description="Retrieve package changes since the last build", - responses={ - 200: {"description": "Success response", "schema": ChangesSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Package base and/or repository are unknown", + schema=ChangesSchema, + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) async def get(self) -> Response: """ get package changes @@ -73,24 +66,17 @@ class ChangesView(StatusViewGuard, BaseView): return json_response(changes.view()) - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Update package changes", description="Update package changes to the new ones", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, + body_schema=ChangesSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(ChangesSchema) async def post(self) -> None: """ insert new package changes diff --git a/src/ahriman/web/views/v1/packages/dependencies.py b/src/ahriman/web/views/v1/packages/dependencies.py index ade5b261..e7654b66 100644 --- a/src/ahriman/web/views/v1/packages/dependencies.py +++ b/src/ahriman/web/views/v1/packages/dependencies.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from ahriman.models.dependencies import Dependencies from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, DependenciesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import DependenciesSchema, PackageNameSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -41,22 +40,16 @@ class DependenciesView(StatusViewGuard, BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/packages/{package}/dependencies"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get package dependencies", description="Retrieve package implicit dependencies", - responses={ - 200: {"description": "Success response", "schema": DependenciesSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Package base and/or repository are unknown", + schema=DependenciesSchema, + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) async def get(self) -> Response: """ get package dependencies @@ -73,24 +66,17 @@ class DependenciesView(StatusViewGuard, BaseView): return json_response(dependencies.view()) - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Update package dependencies", description="Set package implicit dependencies", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, + body_schema=DependenciesSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(DependenciesSchema) async def post(self) -> None: """ insert new package dependencies diff --git a/src/ahriman/web/views/v1/packages/logs.py b/src/ahriman/web/views/v1/packages/logs.py index 3db6754f..d670ef12 100644 --- a/src/ahriman/web/views/v1/packages/logs.py +++ b/src/ahriman/web/views/v1/packages/logs.py @@ -17,16 +17,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from ahriman.core.exceptions import UnknownPackageError from ahriman.core.utils import pretty_datetime from ahriman.models.log_record_id import LogRecordId from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, \ - RepositoryIdSchema, VersionedLogSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema, \ + VersionedLogSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -45,22 +44,15 @@ class LogsView(StatusViewGuard, BaseView): GET_PERMISSION = UserAccess.Reporter ROUTES = ["/api/v1/packages/{package}/logs"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Delete package logs", description="Delete all logs which belong to the specified package", - responses={ - 204: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [DELETE_PERMISSION]}], + permission=DELETE_PERMISSION, + error_404_description="Repository is unknown", + match_schema=PackageNameSchema, + query_schema=PackageVersionSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(PackageVersionSchema) async def delete(self) -> None: """ delete package logs @@ -74,22 +66,16 @@ class LogsView(StatusViewGuard, BaseView): raise HTTPNoContent - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get package logs", description="Retrieve all package logs and the last package status", - responses={ - 200: {"description": "Success response", "schema": LogsSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Package base and/or repository are unknown", + schema=LogsSchema, + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) async def get(self) -> Response: """ get last package logs @@ -115,23 +101,16 @@ class LogsView(StatusViewGuard, BaseView): } return json_response(response) - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Add package logs", description="Insert new package log record", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + match_schema=PackageNameSchema, + body_schema=VersionedLogSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.json_schema(VersionedLogSchema) async def post(self) -> None: """ create new package log record diff --git a/src/ahriman/web/views/v1/packages/package.py b/src/ahriman/web/views/v1/packages/package.py index a2cbddd2..889b8724 100644 --- a/src/ahriman/web/views/v1/packages/package.py +++ b/src/ahriman/web/views/v1/packages/package.py @@ -17,16 +17,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from ahriman.core.exceptions import UnknownPackageError from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package import Package from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, \ - PackageStatusSimplifiedSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackageNameSchema, PackageStatusSchema, PackageStatusSimplifiedSchema, \ + RepositoryIdSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -45,22 +44,15 @@ class PackageView(StatusViewGuard, BaseView): GET_PERMISSION = UserAccess.Read ROUTES = ["/api/v1/packages/{package}"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Delete package", description="Delete package and its status from service", - responses={ - 204: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [DELETE_PERMISSION]}], + permission=DELETE_PERMISSION, + error_404_description="Repository is unknown", + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) async def delete(self) -> None: """ delete package base from status page @@ -73,22 +65,16 @@ class PackageView(StatusViewGuard, BaseView): raise HTTPNoContent - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get package", description="Retrieve packages and its descriptor", - responses={ - 200: {"description": "Success response", "schema": PackageStatusSchema(many=True)}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Package base and/or repository are unknown", + schema=PackageStatusSchema(many=True), + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) async def get(self) -> Response: """ get current package base status @@ -116,24 +102,17 @@ class PackageView(StatusViewGuard, BaseView): ] return json_response(response) - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Update package", description="Update package status and set its descriptior optionally", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + match_schema=PackageNameSchema, + query_schema=RepositoryIdSchema, + body_schema=PackageStatusSimplifiedSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(PackageStatusSimplifiedSchema) async def post(self) -> None: """ update package build status diff --git a/src/ahriman/web/views/v1/packages/packages.py b/src/ahriman/web/views/v1/packages/packages.py index e39cf9ff..fd515e0d 100644 --- a/src/ahriman/web/views/v1/packages/packages.py +++ b/src/ahriman/web/views/v1/packages/packages.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] import itertools from aiohttp.web import HTTPNoContent, Response, json_response @@ -26,7 +25,8 @@ from collections.abc import Callable from ahriman.models.build_status import BuildStatus from ahriman.models.package import Package from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackageStatusSchema, PaginationSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -44,22 +44,16 @@ class PackagesView(StatusViewGuard, BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/packages"] - @aiohttp_apispec.docs( - tags=["Packages"], + @apidocs( + tags=["packages"], summary="Get packages list", description="Retrieve packages and their descriptors", - responses={ - 200: {"description": "Success response", "schema": PackageStatusSchema(many=True)}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + schema=PackageStatusSchema(many=True), + query_schema=PaginationSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(PaginationSchema) async def get(self) -> Response: """ get current packages status @@ -84,21 +78,14 @@ class PackagesView(StatusViewGuard, BaseView): return json_response(response) - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Load packages", description="Load packages from cache", - responses={ - 204: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_404_description="Repository is unknown", + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) async def post(self) -> None: """ reload all packages from repository diff --git a/src/ahriman/web/views/v1/packages/patch.py b/src/ahriman/web/views/v1/packages/patch.py index c0d76018..7021132f 100644 --- a/src/ahriman/web/views/v1/packages/patch.py +++ b/src/ahriman/web/views/v1/packages/patch.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PatchNameSchema, PatchSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PatchNameSchema, PatchSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -40,20 +39,13 @@ class PatchView(StatusViewGuard, BaseView): GET_PERMISSION = UserAccess.Reporter ROUTES = ["/api/v1/packages/{package}/patches/{patch}"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Delete package patch", description="Delete package patch by variable", - responses={ - 204: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [DELETE_PERMISSION]}], + permission=DELETE_PERMISSION, + match_schema=PatchNameSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PatchNameSchema) async def delete(self) -> None: """ delete package patch @@ -68,21 +60,15 @@ class PatchView(StatusViewGuard, BaseView): raise HTTPNoContent - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get package patch", description="Retrieve package patch by variable", - responses={ - 200: {"description": "Success response", "schema": PatchSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Patch name is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Patch name is unknown", + schema=PatchSchema, + match_schema=PatchNameSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PatchNameSchema) async def get(self) -> Response: """ get package patch diff --git a/src/ahriman/web/views/v1/packages/patches.py b/src/ahriman/web/views/v1/packages/patches.py index 23444483..09d6b877 100644 --- a/src/ahriman/web/views/v1/packages/patches.py +++ b/src/ahriman/web/views/v1/packages/patches.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PatchSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackageNameSchema, PatchSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -41,20 +40,14 @@ class PatchesView(StatusViewGuard, BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/packages/{package}/patches"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get package patches", description="Retrieve all package patches", - responses={ - 200: {"description": "Success response", "schema": PatchSchema(many=True)}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + schema=PatchSchema(many=True), + match_schema=PackageNameSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) async def get(self) -> Response: """ get package patches @@ -68,22 +61,15 @@ class PatchesView(StatusViewGuard, BaseView): response = [patch.view() for patch in patches] return json_response(response) - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Update package patch", description="Update or create package patch", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + match_schema=PackageNameSchema, + body_schema=PatchSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.json_schema(PatchSchema) async def post(self) -> None: """ update or create package patch diff --git a/src/ahriman/web/views/v1/service/add.py b/src/ahriman/web/views/v1/service/add.py index d8d4403c..911c097b 100644 --- a/src/ahriman/web/views/v1/service/add.py +++ b/src/ahriman/web/views/v1/service/add.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, Response, json_response from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView @@ -38,23 +37,17 @@ class AddView(BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/service/add"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Add new package", description="Add new package(s) from AUR", - responses={ - 200: {"description": "Success response", "schema": ProcessIdSchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + schema=ProcessIdSchema, + query_schema=RepositoryIdSchema, + body_schema=PackagePatchSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(PackagePatchSchema) async def post(self) -> Response: """ add new package diff --git a/src/ahriman/web/views/v1/service/pgp.py b/src/ahriman/web/views/v1/service/pgp.py index db9719b0..2f8803d8 100644 --- a/src/ahriman/web/views/v1/service/pgp.py +++ b/src/ahriman/web/views/v1/service/pgp.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PGPKeyIdSchema, PGPKeySchema, ProcessIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PGPKeyIdSchema, PGPKeySchema, ProcessIdSchema from ahriman.web.views.base import BaseView @@ -39,22 +38,16 @@ class PGPView(BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/service/pgp"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Search for PGP key", description="Search for PGP key and retrieve its body", - responses={ - 200: {"description": "Success response", "schema": PGPKeySchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "PGP key is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_400_enabled=True, + error_404_description="PGP key is unknown", + schema=PGPKeySchema, + query_schema=PGPKeyIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(PGPKeyIdSchema) async def get(self) -> Response: """ retrieve key from the key server @@ -79,21 +72,15 @@ class PGPView(BaseView): return json_response({"key": key}) - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Fetch PGP key", description="Fetch PGP key from the key server", - responses={ - 200: {"description": "Success response", "schema": ProcessIdSchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + schema=ProcessIdSchema, + body_schema=PGPKeyIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.json_schema(PGPKeyIdSchema) async def post(self) -> Response: """ store key to the local service environment diff --git a/src/ahriman/web/views/v1/service/process.py b/src/ahriman/web/views/v1/service/process.py index 6ce46639..1c963e0a 100644 --- a/src/ahriman/web/views/v1/service/process.py +++ b/src/ahriman/web/views/v1/service/process.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPNotFound, Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, ProcessSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import ProcessIdSchema, ProcessSchema from ahriman.web.views.base import BaseView @@ -37,21 +36,15 @@ class ProcessView(BaseView): GET_PERMISSION = UserAccess.Reporter ROUTES = ["/api/v1/service/process/{process_id}"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Get process", description="Get process information", - responses={ - 200: {"description": "Success response", "schema": ProcessSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Process is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Process is unknown", + schema=ProcessSchema, + match_schema=ProcessIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(ProcessIdSchema) async def get(self) -> Response: """ get spawned process status diff --git a/src/ahriman/web/views/v1/service/rebuild.py b/src/ahriman/web/views/v1/service/rebuild.py index 38120913..37aa708c 100644 --- a/src/ahriman/web/views/v1/service/rebuild.py +++ b/src/ahriman/web/views/v1/service/rebuild.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView @@ -37,23 +36,17 @@ class RebuildView(BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/service/rebuild"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Rebuild packages", description="Rebuild packages which depend on specified one", - responses={ - 200: {"description": "Success response", "schema": ProcessIdSchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + schema=ProcessIdSchema, + query_schema=RepositoryIdSchema, + body_schema=PackageNamesSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(PackageNamesSchema) async def post(self) -> Response: """ rebuild packages based on their dependency diff --git a/src/ahriman/web/views/v1/service/remove.py b/src/ahriman/web/views/v1/service/remove.py index 85719861..e00c5dfe 100644 --- a/src/ahriman/web/views/v1/service/remove.py +++ b/src/ahriman/web/views/v1/service/remove.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView @@ -37,23 +36,17 @@ class RemoveView(BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/service/remove"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Remove packages", description="Remove specified packages from the repository", - responses={ - 200: {"description": "Success response", "schema": ProcessIdSchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + schema=ProcessIdSchema, + query_schema=RepositoryIdSchema, + body_schema=PackageNamesSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(PackageNamesSchema) async def post(self) -> Response: """ remove existing packages diff --git a/src/ahriman/web/views/v1/service/request.py b/src/ahriman/web/views/v1/service/request.py index c24984f1..e92b6127 100644 --- a/src/ahriman/web/views/v1/service/request.py +++ b/src/ahriman/web/views/v1/service/request.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, Response, json_response from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView @@ -38,23 +37,17 @@ class RequestView(BaseView): POST_PERMISSION = UserAccess.Reporter ROUTES = ["/api/v1/service/request"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Request new package", description="Request new package(s) to be added from AUR", - responses={ - 200: {"description": "Success response", "schema": ProcessIdSchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + schema=ProcessIdSchema, + query_schema=RepositoryIdSchema, + body_schema=PackagePatchSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(PackagePatchSchema) async def post(self) -> Response: """ request to add new package diff --git a/src/ahriman/web/views/v1/service/search.py b/src/ahriman/web/views/v1/service/search.py index 2bbf219c..1c4da52a 100644 --- a/src/ahriman/web/views/v1/service/search.py +++ b/src/ahriman/web/views/v1/service/search.py @@ -17,15 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response from collections.abc import Callable from ahriman.core.alpm.remote import AUR from ahriman.models.aur_package import AURPackage from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AURPackageSchema, AuthSchema, ErrorSchema, SearchSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import AURPackageSchema, SearchSchema from ahriman.web.views.base import BaseView @@ -40,22 +39,16 @@ class SearchView(BaseView): GET_PERMISSION = UserAccess.Reporter ROUTES = ["/api/v1/service/search"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Search for package", description="Search for package in AUR", - responses={ - 200: {"description": "Success response", "schema": AURPackageSchema(many=True)}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Package base is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_400_enabled=True, + error_404_description="Package base is unknown", + schema=AURPackageSchema(many=True), + query_schema=SearchSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(SearchSchema) async def get(self) -> Response: """ search packages in AUR diff --git a/src/ahriman/web/views/v1/service/update.py b/src/ahriman/web/views/v1/service/update.py index a18997e2..896f6972 100644 --- a/src/ahriman/web/views/v1/service/update.py +++ b/src/ahriman/web/views/v1/service/update.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, RepositoryIdSchema, UpdateFlagsSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import ProcessIdSchema, RepositoryIdSchema, UpdateFlagsSchema from ahriman.web.views.base import BaseView @@ -37,23 +36,17 @@ class UpdateView(BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/service/update"] - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Update packages", description="Run repository update process", - responses={ - 200: {"description": "Success response", "schema": ProcessIdSchema}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + schema=ProcessIdSchema, + query_schema=RepositoryIdSchema, + body_schema=UpdateFlagsSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(UpdateFlagsSchema) async def post(self) -> Response: """ run repository update. No parameters supported here diff --git a/src/ahriman/web/views/v1/service/upload.py b/src/ahriman/web/views/v1/service/upload.py index eaf652a4..c8c118ff 100644 --- a/src/ahriman/web/views/v1/service/upload.py +++ b/src/ahriman/web/views/v1/service/upload.py @@ -17,17 +17,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] import shutil from aiohttp import BodyPartReader -from aiohttp.web import HTTPBadRequest, HTTPCreated, HTTPNotFound +from aiohttp.web import HTTPBadRequest, HTTPCreated from pathlib import Path from tempfile import NamedTemporaryFile +from ahriman.core.configuration import Configuration from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import FileSchema, RepositoryIdSchema from ahriman.web.views.base import BaseView @@ -42,6 +43,22 @@ class UploadView(BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/service/upload"] + @classmethod + def routes(cls, configuration: Configuration) -> list[str]: + """ + extract routes list for the view + + Args: + configuration(Configuration): configuration instance + + Returns: + list[str]: list of routes defined for the view. By default, it tries to read :attr:`ROUTES` option if set + and returns empty list otherwise + """ + if not configuration.getboolean("web", "enable_archive_upload", fallback=False): + return [] + return cls.ROUTES + @staticmethod async def save_file(part: BodyPartReader, target: Path, *, max_body_size: int | None = None) -> tuple[str, Path]: """ @@ -92,23 +109,18 @@ class UploadView(BaseView): return archive_name, temporary_output - @aiohttp_apispec.docs( + @apidocs( tags=["Actions"], summary="Upload package", description="Upload package to local filesystem", - responses={ - 201: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown or endpoint is disabled", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + response_code=HTTPCreated, + error_400_enabled=True, + error_404_description="Repository is unknown", + query_schema=RepositoryIdSchema, + body_schema=FileSchema, + body_location="form", ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.form_schema(FileSchema) async def post(self) -> None: """ upload file from another instance to the server @@ -118,9 +130,6 @@ class UploadView(BaseView): HTTPCreated: on success response HTTPNotFound: method is disabled by configuration """ - if not self.configuration.getboolean("web", "enable_archive_upload", fallback=False): - raise HTTPNotFound - try: reader = await self.request.multipart() except Exception as ex: diff --git a/src/ahriman/web/views/v1/status/info.py b/src/ahriman/web/views/v1/status/info.py index 988712c6..bc79ae72 100644 --- a/src/ahriman/web/views/v1/status/info.py +++ b/src/ahriman/web/views/v1/status/info.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import Response, json_response from ahriman import __version__ from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, InfoSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import InfoSchema from ahriman.web.views.base import BaseView @@ -38,19 +37,13 @@ class InfoView(BaseView): GET_PERMISSION = UserAccess.Unauthorized ROUTES = ["/api/v1/info"] - @aiohttp_apispec.docs( + @apidocs( tags=["Status"], summary="Service information", description="Perform basic service health check and returns its information", - responses={ - 200: {"description": "Success response", "schema": InfoSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + schema=InfoSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) async def get(self) -> Response: """ get service information diff --git a/src/ahriman/web/views/v1/status/repositories.py b/src/ahriman/web/views/v1/status/repositories.py index 9f099be5..e5d0b733 100644 --- a/src/ahriman/web/views/v1/status/repositories.py +++ b/src/ahriman/web/views/v1/status/repositories.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, RepositoryIdSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import RepositoryIdSchema from ahriman.web.views.base import BaseView @@ -37,19 +36,13 @@ class RepositoriesView(BaseView): GET_PERMISSION = UserAccess.Read ROUTES = ["/api/v1/repositories"] - @aiohttp_apispec.docs( + @apidocs( tags=["Status"], summary="Available repositories", description="List available repositories", - responses={ - 200: {"description": "Success response", "schema": RepositoryIdSchema(many=True)}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + schema=RepositoryIdSchema(many=True), ) - @aiohttp_apispec.cookies_schema(AuthSchema) async def get(self) -> Response: """ get list of available repositories diff --git a/src/ahriman/web/views/v1/status/status.py b/src/ahriman/web/views/v1/status/status.py index 7fd518d9..eb6a6969 100644 --- a/src/ahriman/web/views/v1/status/status.py +++ b/src/ahriman/web/views/v1/status/status.py @@ -17,8 +17,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from ahriman import __version__ @@ -26,7 +24,8 @@ from ahriman.models.build_status import BuildStatusEnum from ahriman.models.counters import Counters from ahriman.models.internal_status import InternalStatus from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, RepositoryIdSchema, StatusSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import InternalStatusSchema, RepositoryIdSchema, StatusSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -44,20 +43,15 @@ class StatusView(StatusViewGuard, BaseView): POST_PERMISSION = UserAccess.Full ROUTES = ["/api/v1/status"] - @aiohttp_apispec.docs( + @apidocs( tags=["Status"], summary="Web service status", description="Get web service status counters", - responses={ - 200: {"description": "Success response", "schema": InternalStatusSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_404_description="Repository is unknown", + schema=InternalStatusSchema, + query_schema=RepositoryIdSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) async def get(self) -> Response: """ get current service status @@ -77,23 +71,16 @@ class StatusView(StatusViewGuard, BaseView): return json_response(status.view()) - @aiohttp_apispec.docs( + @apidocs( tags=["Status"], summary="Set web service status", description="Update web service status. Counters will remain unchanged", - responses={ - 204: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Repository is unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + error_400_enabled=True, + error_404_description="Repository is unknown", + query_schema=RepositoryIdSchema, + body_schema=StatusSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.querystring_schema(RepositoryIdSchema) - @aiohttp_apispec.json_schema(StatusSchema) async def post(self) -> None: """ update service status diff --git a/src/ahriman/web/views/v1/user/login.py b/src/ahriman/web/views/v1/user/login.py index 766e72db..d2ebe32d 100644 --- a/src/ahriman/web/views/v1/user/login.py +++ b/src/ahriman/web/views/v1/user/login.py @@ -17,13 +17,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPBadRequest, HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized from ahriman.core.auth.helpers import remember from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import ErrorSchema, LoginSchema, OAuth2Schema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import LoginSchema, OAuth2Schema from ahriman.web.views.base import BaseView @@ -39,18 +38,14 @@ class LoginView(BaseView): GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized ROUTES = ["/api/v1/login"] - @aiohttp_apispec.docs( + @apidocs( tags=["Login"], summary="Login via OAuth2", description="Login by using OAuth2 authorization code. Only available if OAuth2 is enabled", - responses={ - 302: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + response_code=HTTPFound, + query_schema=OAuth2Schema, ) - @aiohttp_apispec.querystring_schema(OAuth2Schema) async def get(self) -> None: """ OAuth2 response handler @@ -87,19 +82,15 @@ class LoginView(BaseView): raise HTTPUnauthorized - @aiohttp_apispec.docs( + @apidocs( tags=["Login"], summary="Login via basic authorization", description="Login by using username and password", - responses={ - 302: {"description": "Success response"}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + response_code=HTTPFound, + error_400_enabled=True, + body_schema=LoginSchema, ) - @aiohttp_apispec.json_schema(LoginSchema) async def post(self) -> None: """ login user to service. The authentication session will be passed in ``Set-Cookie`` header. diff --git a/src/ahriman/web/views/v1/user/logout.py b/src/ahriman/web/views/v1/user/logout.py index 8ec5ba46..5f4cbcdc 100644 --- a/src/ahriman/web/views/v1/user/logout.py +++ b/src/ahriman/web/views/v1/user/logout.py @@ -17,13 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import HTTPFound, HTTPUnauthorized from ahriman.core.auth.helpers import check_authorized, forget from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema +from ahriman.web.apispec.decorators import apidocs from ahriman.web.views.base import BaseView @@ -38,18 +36,13 @@ class LogoutView(BaseView): POST_PERMISSION = UserAccess.Unauthorized ROUTES = ["/api/v1/logout"] - @aiohttp_apispec.docs( + @apidocs( tags=["Login"], summary="Logout", description="Logout user and remove authorization cookies", - responses={ - 302: {"description": "Success response"}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [POST_PERMISSION]}], + permission=POST_PERMISSION, + response_code=HTTPFound, ) - @aiohttp_apispec.cookies_schema(AuthSchema) async def post(self) -> None: """ logout user from the service diff --git a/src/ahriman/web/views/v2/packages/logs.py b/src/ahriman/web/views/v2/packages/logs.py index 049f9c0f..c94bc373 100644 --- a/src/ahriman/web/views/v2/packages/logs.py +++ b/src/ahriman/web/views/v2/packages/logs.py @@ -17,12 +17,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import aiohttp_apispec # type: ignore[import-untyped] - from aiohttp.web import Response, json_response from ahriman.models.user_access import UserAccess -from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema +from ahriman.web.apispec.decorators import apidocs +from ahriman.web.schemas import LogSchema, PackageNameSchema, PaginationSchema from ahriman.web.views.base import BaseView from ahriman.web.views.status_view_guard import StatusViewGuard @@ -38,23 +37,17 @@ class LogsView(StatusViewGuard, BaseView): GET_PERMISSION = UserAccess.Reporter ROUTES = ["/api/v2/packages/{package}/logs"] - @aiohttp_apispec.docs( + @apidocs( tags=["Packages"], summary="Get paginated package logs", description="Retrieve package logs and the last package status", - responses={ - 200: {"description": "Success response", "schema": LogSchema(many=True)}, - 400: {"description": "Bad data is supplied", "schema": ErrorSchema}, - 401: {"description": "Authorization required", "schema": ErrorSchema}, - 403: {"description": "Access is forbidden", "schema": ErrorSchema}, - 404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema}, - 500: {"description": "Internal server error", "schema": ErrorSchema}, - }, - security=[{"token": [GET_PERMISSION]}], + permission=GET_PERMISSION, + error_400_enabled=True, + error_404_description="Package base and/or repository are unknown", + schema=LogSchema(many=True), + match_schema=PackageNameSchema, + query_schema=PaginationSchema, ) - @aiohttp_apispec.cookies_schema(AuthSchema) - @aiohttp_apispec.match_info_schema(PackageNameSchema) - @aiohttp_apispec.querystring_schema(PaginationSchema) async def get(self) -> Response: """ get last package logs diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index c3297a70..aea6a166 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -33,7 +33,7 @@ from ahriman.core.spawn import Spawn from ahriman.core.status import Client from ahriman.core.status.watcher import Watcher from ahriman.models.repository_id import RepositoryId -from ahriman.web.apispec import setup_apispec +from ahriman.web.apispec.info import setup_apispec from ahriman.web.cors import setup_cors from ahriman.web.keys import AuthKey, ConfigurationKey, SpawnKey, WatcherKey, WorkersKey from ahriman.web.middlewares.exception_handler import exception_handler diff --git a/tests/ahriman/web/apispec/test_apispec.py b/tests/ahriman/web/apispec/test_apispec.py new file mode 100644 index 00000000..02257efd --- /dev/null +++ b/tests/ahriman/web/apispec/test_apispec.py @@ -0,0 +1,24 @@ +import importlib +import sys + +from pytest_mock import MockerFixture +from ahriman.web import apispec + + +def test_import_apispec() -> None: + """ + must correctly import apispec + """ + assert apispec.aiohttp_apispec + + +def test_import_apispec_missing(mocker: MockerFixture) -> None: + """ + must correctly process missing module + """ + mocker.patch.dict(sys.modules, {"aiohttp_apispec": None}) + importlib.reload(apispec) + + assert apispec.aiohttp_apispec is None + assert apispec.Schema + assert apispec.fields("arg", kwargs=42) diff --git a/tests/ahriman/web/apispec/test_decorators.py b/tests/ahriman/web/apispec/test_decorators.py new file mode 100644 index 00000000..3ae29718 --- /dev/null +++ b/tests/ahriman/web/apispec/test_decorators.py @@ -0,0 +1,161 @@ +from aiohttp.web import HTTPFound +from pytest_mock import MockerFixture +from unittest.mock import MagicMock + +from ahriman.models.user_access import UserAccess +from ahriman.web.apispec import decorators +from ahriman.web.apispec.decorators import _response_schema, apidocs +from ahriman.web.schemas import LoginSchema + + +def test_response_schema() -> None: + """ + must generate response schema + """ + schema = _response_schema(None) + assert schema.pop(204) + assert schema.pop(401) + assert schema.pop(403) + assert schema.pop(500) + + +def test_response_schema_no_403() -> None: + """ + must generate response schema without 403 error + """ + schema = _response_schema(None, error_403_enabled=False) + assert 403 not in schema + + +def test_response_schema_400() -> None: + """ + must generate response schema with 400 error + """ + schema = _response_schema(None, error_400_enabled=True) + assert schema.pop(400) + + +def test_response_schema_404() -> None: + """ + must generate response schema with 404 error + """ + schema = _response_schema(None, error_404_description="description") + assert schema.pop(404) + + +def test_response_schema_200() -> None: + """ + must generate response schema with 200 response + """ + schema = _response_schema(LoginSchema) + response = schema.pop(200) + assert response["schema"] == LoginSchema + assert 204 not in schema + + +def test_response_schema_code() -> None: + """ + must override status code + """ + schema = _response_schema(None, response_code=HTTPFound) + assert schema.pop(302) + assert 204 not in schema + + +def test_apidocs() -> None: + """ + must return decorated function + """ + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Unauthorized, + )(MagicMock()) + assert annotated.__apispec__ + + +def test_apidocs_authorization() -> None: + """ + must return decorated function with authorization details + """ + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Full, + )(MagicMock()) + assert any(schema["put_into"] == "cookies" for schema in annotated.__schemas__) + + +def test_apidocs_match() -> None: + """ + must return decorated function with match details + """ + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Unauthorized, + match_schema=LoginSchema, + )(MagicMock()) + assert any(schema["put_into"] == "match_info" for schema in annotated.__schemas__) + + +def test_apidocs_querystring() -> None: + """ + must return decorated function with query string details + """ + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Unauthorized, + query_schema=LoginSchema, + )(MagicMock()) + assert any(schema["put_into"] == "querystring" for schema in annotated.__schemas__) + + +def test_apidocs_json() -> None: + """ + must return decorated function with json details + """ + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Unauthorized, + body_schema=LoginSchema, + )(MagicMock()) + assert any(schema["put_into"] == "json" for schema in annotated.__schemas__) + + +def test_apidocs_form() -> None: + """ + must return decorated function with generic body details + """ + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Unauthorized, + body_schema=LoginSchema, + body_location="form", + )(MagicMock()) + assert any(schema["put_into"] == "form" for schema in annotated.__schemas__) + + +def test_apidocs_import_error(mocker: MockerFixture) -> None: + """ + must return same function if no apispec module available + """ + mocker.patch.object(decorators, "aiohttp_apispec", None) + mock = MagicMock() + + annotated = apidocs( + tags=["tags"], + summary="summary", + description="description", + permission=UserAccess.Unauthorized, + )(mock) + assert annotated == mock diff --git a/tests/ahriman/web/test_apispec.py b/tests/ahriman/web/apispec/test_info.py similarity index 78% rename from tests/ahriman/web/test_apispec.py rename to tests/ahriman/web/apispec/test_info.py index 47df2ca9..bfde0549 100644 --- a/tests/ahriman/web/test_apispec.py +++ b/tests/ahriman/web/apispec/test_info.py @@ -4,7 +4,8 @@ from aiohttp.web import Application from pytest_mock import MockerFixture from ahriman import __version__ -from ahriman.web.apispec import _info, _security, _servers, setup_apispec +from ahriman.web.apispec import info +from ahriman.web.apispec.info import _info, _security, _servers, setup_apispec from ahriman.web.keys import ConfigurationKey @@ -47,7 +48,7 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None: must set api specification """ apispec_mock = mocker.patch("aiohttp_apispec.setup_aiohttp_apispec") - setup_apispec(application) + assert setup_apispec(application) apispec_mock.assert_called_once_with( application, url="/api-docs/swagger.json", @@ -56,3 +57,11 @@ def test_setup_apispec(application: Application, mocker: MockerFixture) -> None: servers=pytest.helpers.anyvar(int), security=pytest.helpers.anyvar(int), ) + + +def test_setup_apispec_import_error(application: Application, mocker: MockerFixture) -> None: + """ + must return none if apispec is not available + """ + mocker.patch.object(info, "aiohttp_apispec", None) + assert setup_apispec(application) is None diff --git a/tests/ahriman/web/views/api/test_view_api_docs.py b/tests/ahriman/web/views/api/test_view_api_docs.py index a1b8d32b..4a959a09 100644 --- a/tests/ahriman/web/views/api/test_view_api_docs.py +++ b/tests/ahriman/web/views/api/test_view_api_docs.py @@ -1,7 +1,9 @@ import pytest from aiohttp.test_utils import TestClient +from pytest_mock import MockerFixture +from ahriman.core.configuration import Configuration from ahriman.models.user_access import UserAccess from ahriman.web.views.api.docs import DocsView @@ -15,6 +17,28 @@ async def test_get_permission() -> None: assert await DocsView.get_permission(request) == UserAccess.Unauthorized +def test_routes() -> None: + """ + must return correct routes + """ + assert DocsView.ROUTES == ["/api-docs"] + + +def test_routes_dynamic(configuration: Configuration) -> None: + """ + must correctly return docs route + """ + assert DocsView.ROUTES == DocsView.routes(configuration) + + +def test_routes_dynamic_not_found(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must disable docs route if no apispec package found + """ + mocker.patch("ahriman.web.views.api.docs.aiohttp_apispec", None) + assert DocsView.routes(configuration) == [] + + async def test_get(client: TestClient) -> None: """ must generate api-docs correctly diff --git a/tests/ahriman/web/views/api/test_view_api_swagger.py b/tests/ahriman/web/views/api/test_view_api_swagger.py index 5d39e977..b03e8a45 100644 --- a/tests/ahriman/web/views/api/test_view_api_swagger.py +++ b/tests/ahriman/web/views/api/test_view_api_swagger.py @@ -4,6 +4,7 @@ from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture from typing import Any +from ahriman.core.configuration import Configuration from ahriman.models.user_access import UserAccess from ahriman.web.views.api.swagger import SwaggerView @@ -86,6 +87,28 @@ async def test_get_permission() -> None: assert await SwaggerView.get_permission(request) == UserAccess.Unauthorized +def test_routes() -> None: + """ + must return correct routes + """ + assert SwaggerView.ROUTES == ["/api-docs/swagger.json"] + + +def test_routes_dynamic(configuration: Configuration) -> None: + """ + must correctly return openapi url + """ + assert SwaggerView.ROUTES == SwaggerView.routes(configuration) + + +def test_routes_dynamic_not_found(configuration: Configuration, mocker: MockerFixture) -> None: + """ + must disable openapi route if no apispec package found + """ + mocker.patch("ahriman.web.views.api.swagger.aiohttp_apispec", None) + assert SwaggerView.routes(configuration) == [] + + async def test_get(client: TestClient, mocker: MockerFixture) -> None: """ must generate api-docs correctly diff --git a/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py index 8e4ed26b..7ab7f41a 100644 --- a/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py @@ -8,6 +8,7 @@ from pathlib import Path from pytest_mock import MockerFixture from unittest.mock import AsyncMock, MagicMock, call as MockCall +from ahriman.core.configuration import Configuration from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.user_access import UserAccess from ahriman.web.views.v1.service.upload import UploadView @@ -29,6 +30,21 @@ def test_routes() -> None: assert UploadView.ROUTES == ["/api/v1/service/upload"] +def test_routes_dynamic(configuration: Configuration) -> None: + """ + must correctly return upload url + """ + assert UploadView.ROUTES == UploadView.routes(configuration) + + +def test_routes_dynamic_not_found(configuration: Configuration) -> None: + """ + must disable upload route if option is not set + """ + configuration.set_option("web", "enable_archive_upload", "no") + assert UploadView.routes(configuration) == [] + + async def test_save_file(mocker: MockerFixture) -> None: """ must correctly save file @@ -134,20 +150,6 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat ]) -async def test_post_not_found(client: TestClient, mocker: MockerFixture) -> None: - """ - must return 404 if request was disabled - """ - mocker.patch("ahriman.core.configuration.Configuration.getboolean", return_value=False) - data = FormData() - data.add_field("package", BytesIO(b"content"), filename="filename", content_type="application/octet-stream") - response_schema = pytest.helpers.schema_response(UploadView.post, code=404) - - response = await client.post("/api/v1/service/upload", data=data) - assert response.status == 404 - assert not response_schema.validate(await response.json()) - - async def test_post_not_multipart(client: TestClient) -> None: """ must return 400 on invalid payload