diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD index 446bd95a..ebac934d 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' 'python-marshmallow') 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/src/ahriman/web/apispec/__init__.py b/src/ahriman/web/apispec/__init__.py new file mode 100644 index 00000000..2c0869b9 --- /dev/null +++ b/src/ahriman/web/apispec/__init__.py @@ -0,0 +1,23 @@ +# +# 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] +except ImportError: + aiohttp_apispec = None diff --git a/src/ahriman/web/apispec/decorators.py b/src/ahriman/web/apispec/decorators.py new file mode 100644 index 00000000..e182b9a7 --- /dev/null +++ b/src/ahriman/web/apispec/decorators.py @@ -0,0 +1,136 @@ +# +# 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 marshmallow import Schema +from typing import Any, Callable + +from ahriman.models.user_access import UserAccess +from ahriman.web.apispec import aiohttp_apispec +from ahriman.web.schemas import AuthSchema, ErrorSchema + + +__all__ = ["apidocs"] + + +def _response_schema(response: Schema | type[Schema] | None, code: int | 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 + code(int | 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}, + } + + if code is None: + code = 200 if response is not None else 204 + 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, + code: int | 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 + code(int | 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) + permission(UserAccess, optional): permission to access endpoint (Default value = UserAccess.Unauthorized) + 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 + + handler = aiohttp_apispec.docs( + tags=tags, + summary=summary, + description=description, + responses=_response_schema(schema, code, error_400_enabled, authorization_required, error_404_description), + 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/spec.py similarity index 94% rename from src/ahriman/web/apispec.py rename to src/ahriman/web/apispec/spec.py index 5c825772..67756c2c 100644 --- a/src/ahriman/web/apispec.py +++ b/src/ahriman/web/apispec/spec.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/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..b59869d4 100644 --- a/src/ahriman/web/views/v1/service/upload.py +++ b/src/ahriman/web/views/v1/service/upload.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 shutil from aiohttp import BodyPartReader @@ -27,7 +26,8 @@ from tempfile import NamedTemporaryFile 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 @@ -92,23 +92,17 @@ 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, + error_400_enabled=True, + error_404_description="Repository is unknown or endpoint is disabled", + 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 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..99c16566 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, + code=302, + 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, + code=302, + 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..4ceafaa7 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, + code=302, ) - @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..2036ebc9 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.spec 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_decorators.py b/tests/ahriman/web/apispec/test_decorators.py new file mode 100644 index 00000000..ad074eb1 --- /dev/null +++ b/tests/ahriman/web/apispec/test_decorators.py @@ -0,0 +1,160 @@ +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, code=302) + 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/apispec/test_init.py b/tests/ahriman/web/apispec/test_init.py new file mode 100644 index 00000000..86632d54 --- /dev/null +++ b/tests/ahriman/web/apispec/test_init.py @@ -0,0 +1,21 @@ +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 diff --git a/tests/ahriman/web/test_apispec.py b/tests/ahriman/web/apispec/test_spec.py similarity index 78% rename from tests/ahriman/web/test_apispec.py rename to tests/ahriman/web/apispec/test_spec.py index 47df2ca9..6e07d3a2 100644 --- a/tests/ahriman/web/test_apispec.py +++ b/tests/ahriman/web/apispec/test_spec.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 spec +from ahriman.web.apispec.spec 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(spec, "aiohttp_apispec", None) + assert setup_apispec(application) is None diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py index 379ebae9..7c9371a2 100644 --- a/tests/ahriman/web/conftest.py +++ b/tests/ahriman/web/conftest.py @@ -90,6 +90,8 @@ def schema_request(handler: Callable[..., Awaitable[Any]], *, location: str = "j Schema: request schema as set by the decorators """ schemas: list[dict[str, Any]] = handler.__schemas__ # type: ignore[attr-defined] + help(handler) + print(schemas) return next(schema["schema"] for schema in schemas if schema["put_into"] == location)