From 99eecdebf3ca98933cba9374a9219fc800b46b34 Mon Sep 17 00:00:00 2001 From: Evgenii Alekseev Date: Thu, 7 Sep 2023 18:16:39 +0300 Subject: [PATCH] feat: pagination support for logs request --- CONTRIBUTING.md | 8 +- docs/ahriman.web.schemas.rst | 8 ++ docs/ahriman.web.views.rst | 5 +- docs/ahriman.web.views.service.rst | 85 ----------------- docs/ahriman.web.views.status.rst | 45 --------- docs/ahriman.web.views.user.rst | 29 ------ docs/ahriman.web.views.v1.rst | 20 ++++ docs/ahriman.web.views.v1.service.rst | 85 +++++++++++++++++ docs/ahriman.web.views.v1.status.rst | 45 +++++++++ docs/ahriman.web.views.v1.user.rst | 29 ++++++ docs/ahriman.web.views.v2.rst | 18 ++++ docs/ahriman.web.views.v2.status.rst | 21 +++++ .../build-status/package-info-modal.jinja2 | 8 +- .../share/ahriman/templates/repo-index.jinja2 | 6 +- .../database/operations/logs_operations.py | 22 +++-- src/ahriman/core/status/watcher.py | 8 +- src/ahriman/web/routes.py | 47 ++++------ src/ahriman/web/schemas/__init__.py | 3 +- src/ahriman/web/schemas/logs_schema.py | 18 ++++ src/ahriman/web/schemas/pagination_schema.py | 35 +++++++ src/ahriman/web/views/v1/__init__.py | 36 +++++++ .../web/views/{ => v1}/service/__init__.py | 0 src/ahriman/web/views/{ => v1}/service/add.py | 0 src/ahriman/web/views/{ => v1}/service/pgp.py | 0 .../web/views/{ => v1}/service/process.py | 0 .../web/views/{ => v1}/service/rebuild.py | 0 .../web/views/{ => v1}/service/remove.py | 0 .../web/views/{ => v1}/service/request.py | 0 .../web/views/{ => v1}/service/search.py | 0 .../web/views/{ => v1}/service/update.py | 0 .../web/views/{ => v1}/service/upload.py | 0 .../web/views/{ => v1}/status/__init__.py | 0 src/ahriman/web/views/{ => v1}/status/logs.py | 3 +- .../web/views/{ => v1}/status/package.py | 0 .../web/views/{ => v1}/status/packages.py | 0 .../web/views/{ => v1}/status/status.py | 0 .../web/views/{ => v1}/user/__init__.py | 0 src/ahriman/web/views/{ => v1}/user/login.py | 0 src/ahriman/web/views/{ => v1}/user/logout.py | 0 src/ahriman/web/views/v2/__init__.py | 20 ++++ src/ahriman/web/views/v2/status/__init__.py | 19 ++++ src/ahriman/web/views/v2/status/logs.py | 87 +++++++++++++++++ .../operations/test_logs_operations.py | 17 +++- tests/ahriman/core/status/test_watcher.py | 4 +- tests/ahriman/test_tests.py | 27 ++++++ .../web/schemas/test_pagination_schema.py | 1 + ...iews_api_docs.py => test_view_api_docs.py} | 0 ...pi_swagger.py => test_view_api_swagger.py} | 0 .../{test_views_base.py => test_view_base.py} | 0 ...test_views_index.py => test_view_index.py} | 0 .../service/test_view_v1_service_add.py} | 2 +- .../service/test_view_v1_service_pgp.py} | 2 +- .../service/test_view_v1_service_process.py} | 2 +- .../service/test_view_v1_service_rebuild.py} | 2 +- .../service/test_view_v1_service_remove.py} | 2 +- .../service/test_view_v1_service_request.py} | 2 +- .../service/test_view_v1_service_search.py} | 2 +- .../service/test_view_v1_service_update.py} | 2 +- .../service/test_view_v1_service_upload.py} | 8 +- .../status/test_view_v1_status_logs.py} | 2 +- .../status/test_view_v1_status_package.py} | 2 +- .../status/test_view_v1_status_packages.py} | 2 +- .../status/test_view_v1_status_status.py} | 2 +- .../user/test_view_v1_user_login.py} | 2 +- .../user/test_view_v1_user_logout.py} | 2 +- .../v2/status/test_view_v2_status_logs.py | 93 +++++++++++++++++++ 66 files changed, 650 insertions(+), 238 deletions(-) delete mode 100644 docs/ahriman.web.views.service.rst delete mode 100644 docs/ahriman.web.views.status.rst delete mode 100644 docs/ahriman.web.views.user.rst create mode 100644 docs/ahriman.web.views.v1.rst create mode 100644 docs/ahriman.web.views.v1.service.rst create mode 100644 docs/ahriman.web.views.v1.status.rst create mode 100644 docs/ahriman.web.views.v1.user.rst create mode 100644 docs/ahriman.web.views.v2.rst create mode 100644 docs/ahriman.web.views.v2.status.rst create mode 100644 src/ahriman/web/schemas/pagination_schema.py create mode 100644 src/ahriman/web/views/v1/__init__.py rename src/ahriman/web/views/{ => v1}/service/__init__.py (100%) rename src/ahriman/web/views/{ => v1}/service/add.py (100%) rename src/ahriman/web/views/{ => v1}/service/pgp.py (100%) rename src/ahriman/web/views/{ => v1}/service/process.py (100%) rename src/ahriman/web/views/{ => v1}/service/rebuild.py (100%) rename src/ahriman/web/views/{ => v1}/service/remove.py (100%) rename src/ahriman/web/views/{ => v1}/service/request.py (100%) rename src/ahriman/web/views/{ => v1}/service/search.py (100%) rename src/ahriman/web/views/{ => v1}/service/update.py (100%) rename src/ahriman/web/views/{ => v1}/service/upload.py (100%) rename src/ahriman/web/views/{ => v1}/status/__init__.py (100%) rename src/ahriman/web/views/{ => v1}/status/logs.py (97%) rename src/ahriman/web/views/{ => v1}/status/package.py (100%) rename src/ahriman/web/views/{ => v1}/status/packages.py (100%) rename src/ahriman/web/views/{ => v1}/status/status.py (100%) rename src/ahriman/web/views/{ => v1}/user/__init__.py (100%) rename src/ahriman/web/views/{ => v1}/user/login.py (100%) rename src/ahriman/web/views/{ => v1}/user/logout.py (100%) create mode 100644 src/ahriman/web/views/v2/__init__.py create mode 100644 src/ahriman/web/views/v2/status/__init__.py create mode 100644 src/ahriman/web/views/v2/status/logs.py create mode 100644 tests/ahriman/test_tests.py create mode 100644 tests/ahriman/web/schemas/test_pagination_schema.py rename tests/ahriman/web/views/api/{test_views_api_docs.py => test_view_api_docs.py} (100%) rename tests/ahriman/web/views/api/{test_views_api_swagger.py => test_view_api_swagger.py} (100%) rename tests/ahriman/web/views/{test_views_base.py => test_view_base.py} (100%) rename tests/ahriman/web/views/{test_views_index.py => test_view_index.py} (100%) rename tests/ahriman/web/views/{service/test_views_service_add.py => v1/service/test_view_v1_service_add.py} (97%) rename tests/ahriman/web/views/{service/test_views_service_pgp.py => v1/service/test_view_v1_service_pgp.py} (98%) rename tests/ahriman/web/views/{service/test_views_service_process.py => v1/service/test_view_v1_service_process.py} (96%) rename tests/ahriman/web/views/{service/test_views_service_rebuild.py => v1/service/test_view_v1_service_rebuild.py} (96%) rename tests/ahriman/web/views/{service/test_views_service_remove.py => v1/service/test_view_v1_service_remove.py} (96%) rename tests/ahriman/web/views/{service/test_views_service_request.py => v1/service/test_view_v1_service_request.py} (96%) rename tests/ahriman/web/views/{service/test_views_service_search.py => v1/service/test_view_v1_service_search.py} (98%) rename tests/ahriman/web/views/{service/test_views_service_update.py => v1/service/test_view_v1_service_update.py} (97%) rename tests/ahriman/web/views/{service/test_views_service_upload.py => v1/service/test_view_v1_service_upload.py} (95%) rename tests/ahriman/web/views/{status/test_views_status_logs.py => v1/status/test_view_v1_status_logs.py} (98%) rename tests/ahriman/web/views/{status/test_views_status_package.py => v1/status/test_view_v1_status_package.py} (99%) rename tests/ahriman/web/views/{status/test_views_status_packages.py => v1/status/test_view_v1_status_packages.py} (97%) rename tests/ahriman/web/views/{status/test_views_status_status.py => v1/status/test_view_v1_status_status.py} (98%) rename tests/ahriman/web/views/{user/test_views_user_login.py => v1/user/test_view_v1_user_login.py} (99%) rename tests/ahriman/web/views/{user/test_views_user_logout.py => v1/user/test_view_v1_user_logout.py} (96%) create mode 100644 tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ab42419..68e5a93a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -174,9 +174,7 @@ Again, the most checks can be performed by `make check` command, though some add from marshmallow import Schema, fields - from ahriman.web.schemas.auth_schema import AuthSchema - from ahriman.web.schemas.error_schema import ErrorSchema - from ahriman.web.schemas.package_name_schema import PackageNameSchema + from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PaginationSchema from ahriman.web.views.base import BaseView @@ -210,10 +208,14 @@ Again, the most checks can be performed by `make check` command, though some add ) @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: ... ``` +* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced. +* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release. + ### Other checks The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests. diff --git a/docs/ahriman.web.schemas.rst b/docs/ahriman.web.schemas.rst index 3f510565..ac798f3e 100644 --- a/docs/ahriman.web.schemas.rst +++ b/docs/ahriman.web.schemas.rst @@ -124,6 +124,14 @@ ahriman.web.schemas.package\_status\_schema module :no-undoc-members: :show-inheritance: +ahriman.web.schemas.pagination\_schema module +--------------------------------------------- + +.. automodule:: ahriman.web.schemas.pagination_schema + :members: + :no-undoc-members: + :show-inheritance: + ahriman.web.schemas.pgp\_key\_id\_schema module ----------------------------------------------- diff --git a/docs/ahriman.web.views.rst b/docs/ahriman.web.views.rst index ccd4a8ee..abe48061 100644 --- a/docs/ahriman.web.views.rst +++ b/docs/ahriman.web.views.rst @@ -8,9 +8,8 @@ Subpackages :maxdepth: 4 ahriman.web.views.api - ahriman.web.views.service - ahriman.web.views.status - ahriman.web.views.user + ahriman.web.views.v1 + ahriman.web.views.v2 Submodules ---------- diff --git a/docs/ahriman.web.views.service.rst b/docs/ahriman.web.views.service.rst deleted file mode 100644 index 542a370c..00000000 --- a/docs/ahriman.web.views.service.rst +++ /dev/null @@ -1,85 +0,0 @@ -ahriman.web.views.service package -================================= - -Submodules ----------- - -ahriman.web.views.service.add module ------------------------------------- - -.. automodule:: ahriman.web.views.service.add - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.pgp module ------------------------------------- - -.. automodule:: ahriman.web.views.service.pgp - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.process module ----------------------------------------- - -.. automodule:: ahriman.web.views.service.process - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.rebuild module ----------------------------------------- - -.. automodule:: ahriman.web.views.service.rebuild - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.remove module ---------------------------------------- - -.. automodule:: ahriman.web.views.service.remove - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.request module ----------------------------------------- - -.. automodule:: ahriman.web.views.service.request - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.search module ---------------------------------------- - -.. automodule:: ahriman.web.views.service.search - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.update module ---------------------------------------- - -.. automodule:: ahriman.web.views.service.update - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.service.upload module ---------------------------------------- - -.. automodule:: ahriman.web.views.service.upload - :members: - :no-undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: ahriman.web.views.service - :members: - :no-undoc-members: - :show-inheritance: diff --git a/docs/ahriman.web.views.status.rst b/docs/ahriman.web.views.status.rst deleted file mode 100644 index ba44c277..00000000 --- a/docs/ahriman.web.views.status.rst +++ /dev/null @@ -1,45 +0,0 @@ -ahriman.web.views.status package -================================ - -Submodules ----------- - -ahriman.web.views.status.logs module ------------------------------------- - -.. automodule:: ahriman.web.views.status.logs - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.status.package module ---------------------------------------- - -.. automodule:: ahriman.web.views.status.package - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.status.packages module ----------------------------------------- - -.. automodule:: ahriman.web.views.status.packages - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.status.status module --------------------------------------- - -.. automodule:: ahriman.web.views.status.status - :members: - :no-undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: ahriman.web.views.status - :members: - :no-undoc-members: - :show-inheritance: diff --git a/docs/ahriman.web.views.user.rst b/docs/ahriman.web.views.user.rst deleted file mode 100644 index 721442e3..00000000 --- a/docs/ahriman.web.views.user.rst +++ /dev/null @@ -1,29 +0,0 @@ -ahriman.web.views.user package -============================== - -Submodules ----------- - -ahriman.web.views.user.login module ------------------------------------ - -.. automodule:: ahriman.web.views.user.login - :members: - :no-undoc-members: - :show-inheritance: - -ahriman.web.views.user.logout module ------------------------------------- - -.. automodule:: ahriman.web.views.user.logout - :members: - :no-undoc-members: - :show-inheritance: - -Module contents ---------------- - -.. automodule:: ahriman.web.views.user - :members: - :no-undoc-members: - :show-inheritance: diff --git a/docs/ahriman.web.views.v1.rst b/docs/ahriman.web.views.v1.rst new file mode 100644 index 00000000..64b29734 --- /dev/null +++ b/docs/ahriman.web.views.v1.rst @@ -0,0 +1,20 @@ +ahriman.web.views.v1 package +============================ + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + ahriman.web.views.v1.service + ahriman.web.views.v1.status + ahriman.web.views.v1.user + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v1 + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.views.v1.service.rst b/docs/ahriman.web.views.v1.service.rst new file mode 100644 index 00000000..29f3c3a0 --- /dev/null +++ b/docs/ahriman.web.views.v1.service.rst @@ -0,0 +1,85 @@ +ahriman.web.views.v1.service package +==================================== + +Submodules +---------- + +ahriman.web.views.v1.service.add module +--------------------------------------- + +.. automodule:: ahriman.web.views.v1.service.add + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.pgp module +--------------------------------------- + +.. automodule:: ahriman.web.views.v1.service.pgp + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.process module +------------------------------------------- + +.. automodule:: ahriman.web.views.v1.service.process + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.rebuild module +------------------------------------------- + +.. automodule:: ahriman.web.views.v1.service.rebuild + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.remove module +------------------------------------------ + +.. automodule:: ahriman.web.views.v1.service.remove + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.request module +------------------------------------------- + +.. automodule:: ahriman.web.views.v1.service.request + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.search module +------------------------------------------ + +.. automodule:: ahriman.web.views.v1.service.search + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.update module +------------------------------------------ + +.. automodule:: ahriman.web.views.v1.service.update + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.service.upload module +------------------------------------------ + +.. automodule:: ahriman.web.views.v1.service.upload + :members: + :no-undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v1.service + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.views.v1.status.rst b/docs/ahriman.web.views.v1.status.rst new file mode 100644 index 00000000..30ad474d --- /dev/null +++ b/docs/ahriman.web.views.v1.status.rst @@ -0,0 +1,45 @@ +ahriman.web.views.v1.status package +=================================== + +Submodules +---------- + +ahriman.web.views.v1.status.logs module +--------------------------------------- + +.. automodule:: ahriman.web.views.v1.status.logs + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.status.package module +------------------------------------------ + +.. automodule:: ahriman.web.views.v1.status.package + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.status.packages module +------------------------------------------- + +.. automodule:: ahriman.web.views.v1.status.packages + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.status.status module +----------------------------------------- + +.. automodule:: ahriman.web.views.v1.status.status + :members: + :no-undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v1.status + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.views.v1.user.rst b/docs/ahriman.web.views.v1.user.rst new file mode 100644 index 00000000..9e338f85 --- /dev/null +++ b/docs/ahriman.web.views.v1.user.rst @@ -0,0 +1,29 @@ +ahriman.web.views.v1.user package +================================= + +Submodules +---------- + +ahriman.web.views.v1.user.login module +-------------------------------------- + +.. automodule:: ahriman.web.views.v1.user.login + :members: + :no-undoc-members: + :show-inheritance: + +ahriman.web.views.v1.user.logout module +--------------------------------------- + +.. automodule:: ahriman.web.views.v1.user.logout + :members: + :no-undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v1.user + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.views.v2.rst b/docs/ahriman.web.views.v2.rst new file mode 100644 index 00000000..fece3d4b --- /dev/null +++ b/docs/ahriman.web.views.v2.rst @@ -0,0 +1,18 @@ +ahriman.web.views.v2 package +============================ + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + ahriman.web.views.v2.status + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v2 + :members: + :no-undoc-members: + :show-inheritance: diff --git a/docs/ahriman.web.views.v2.status.rst b/docs/ahriman.web.views.v2.status.rst new file mode 100644 index 00000000..1d777ebb --- /dev/null +++ b/docs/ahriman.web.views.v2.status.rst @@ -0,0 +1,21 @@ +ahriman.web.views.v2.status package +=================================== + +Submodules +---------- + +ahriman.web.views.v2.status.logs module +--------------------------------------- + +.. automodule:: ahriman.web.views.v2.status.logs + :members: + :no-undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: ahriman.web.views.v2.status + :members: + :no-undoc-members: + :show-inheritance: diff --git a/package/share/ahriman/templates/build-status/package-info-modal.jinja2 b/package/share/ahriman/templates/build-status/package-info-modal.jinja2 index e3eb5ff5..38560a86 100644 --- a/package/share/ahriman/templates/build-status/package-info-modal.jinja2 +++ b/package/share/ahriman/templates/build-status/package-info-modal.jinja2 @@ -45,12 +45,16 @@ }; $.ajax({ - url: `/api/v1/packages/${packageBase}/logs`, + url: `/api/v2/packages/${packageBase}/logs`, type: "GET", dataType: "json", success: response => { packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`); - packageInfoLogsInput.text(response.logs); + const logs = response.logs.map(log_record => { + const [timestamp, record] = log_record; + return `[${new Date(1000 * timestamp).toISOString()}] ${record}`; + }); + packageInfoLogsInput.text(logs.join("\n")); packageInfoModalHeader.removeClass(); packageInfoModalHeader.addClass("modal-header"); diff --git a/package/share/ahriman/templates/repo-index.jinja2 b/package/share/ahriman/templates/repo-index.jinja2 index ed8cfcd6..4090a683 100644 --- a/package/share/ahriman/templates/repo-index.jinja2 +++ b/package/share/ahriman/templates/repo-index.jinja2 @@ -73,9 +73,9 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa {{ package.architecture }} {{ package.description }} {{ package.url }} - {{ package.licenses|join("
"|safe) }} - {{ package.groups|join("
"|safe) }} - {{ package.depends|join("
"|safe) }} + {{ package.licenses | join("
" | safe) }} + {{ package.groups | join("
" | safe) }} + {{ package.depends | join("
" | safe) }} {{ package.archive_size }} {{ package.installed_size }} {{ package.build_date }} diff --git a/src/ahriman/core/database/operations/logs_operations.py b/src/ahriman/core/database/operations/logs_operations.py index 202d1eb8..1ecd2563 100644 --- a/src/ahriman/core/database/operations/logs_operations.py +++ b/src/ahriman/core/database/operations/logs_operations.py @@ -20,7 +20,6 @@ from sqlite3 import Connection from ahriman.core.database.operations import Operations -from ahriman.core.util import pretty_datetime from ahriman.models.log_record_id import LogRecordId @@ -29,29 +28,34 @@ class LogsOperations(Operations): logs operations """ - def logs_get(self, package_base: str) -> str: + def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]: """ extract logs for specified package base Args: package_base(str): package base to extract logs + limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1) + offset(int, optional): records offset (Default value = 0) Return: - str: full package log + list[tuple[float, str]]: sorted package log records and their timestamps """ - def run(connection: Connection) -> list[str]: + def run(connection: Connection) -> list[tuple[float, str]]: return [ - f"""[{pretty_datetime(row["created"])}] {row["record"]}""" + (row["created"], row["record"]) for row in connection.execute( """ select created, record from logs where package_base = :package_base - order by created + order by created limit :limit offset :offset """, - {"package_base": package_base}) + { + "package_base": package_base, + "limit": limit, + "offset": offset, + }) ] - records = self.with_connection(run) - return "\n".join(records) + return self.with_connection(run) def logs_insert(self, log_record_id: LogRecordId, created: float, record: str) -> None: """ diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py index d2ec7cad..3ea03c4c 100644 --- a/src/ahriman/core/status/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -85,17 +85,19 @@ class Watcher(LazyLogging): if package.base in self.known: self.known[package.base] = (package, status) - def logs_get(self, package_base: str) -> str: + def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]: """ extract logs for the package base Args: package_base(str): package base + limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1) + offset(int, optional): records offset (Default value = 0) Returns: - str: package logs + list[tuple[float, str]]: package logs """ - return self.database.logs_get(package_base) + return self.database.logs_get(package_base, limit, offset) def logs_remove(self, package_base: str, version: str | None) -> None: """ diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py index 7a34d330..ed04d7e4 100644 --- a/src/ahriman/web/routes.py +++ b/src/ahriman/web/routes.py @@ -23,21 +23,7 @@ from pathlib import Path from ahriman.web.views.api.docs import DocsView from ahriman.web.views.api.swagger import SwaggerView from ahriman.web.views.index import IndexView -from ahriman.web.views.service.add import AddView -from ahriman.web.views.service.pgp import PGPView -from ahriman.web.views.service.process import ProcessView -from ahriman.web.views.service.rebuild import RebuildView -from ahriman.web.views.service.remove import RemoveView -from ahriman.web.views.service.request import RequestView -from ahriman.web.views.service.search import SearchView -from ahriman.web.views.service.update import UpdateView -from ahriman.web.views.service.upload import UploadView -from ahriman.web.views.status.logs import LogsView -from ahriman.web.views.status.package import PackageView -from ahriman.web.views.status.packages import PackagesView -from ahriman.web.views.status.status import StatusView -from ahriman.web.views.user.login import LoginView -from ahriman.web.views.user.logout import LogoutView +from ahriman.web.views import v1, v2 __all__ = ["setup_routes"] @@ -59,21 +45,22 @@ def setup_routes(application: Application, static_path: Path) -> None: application.router.add_static("/static", static_path, follow_symlinks=True) - application.router.add_view("/api/v1/service/add", AddView) - application.router.add_view("/api/v1/service/pgp", PGPView) - application.router.add_view("/api/v1/service/rebuild", RebuildView) - application.router.add_view("/api/v1/service/process/{process_id}", ProcessView) - application.router.add_view("/api/v1/service/remove", RemoveView) - application.router.add_view("/api/v1/service/request", RequestView) - application.router.add_view("/api/v1/service/search", SearchView) - application.router.add_view("/api/v1/service/update", UpdateView) - application.router.add_view("/api/v1/service/upload", UploadView) + application.router.add_view("/api/v1/service/add", v1.AddView) + application.router.add_view("/api/v1/service/pgp", v1.PGPView) + application.router.add_view("/api/v1/service/rebuild", v1.RebuildView) + application.router.add_view("/api/v1/service/process/{process_id}", v1.ProcessView) + application.router.add_view("/api/v1/service/remove", v1.RemoveView) + application.router.add_view("/api/v1/service/request", v1.RequestView) + application.router.add_view("/api/v1/service/search", v1.SearchView) + application.router.add_view("/api/v1/service/update", v1.UpdateView) + application.router.add_view("/api/v1/service/upload", v1.UploadView) - application.router.add_view("/api/v1/packages", PackagesView) - application.router.add_view("/api/v1/packages/{package}", PackageView) - application.router.add_view("/api/v1/packages/{package}/logs", LogsView) + application.router.add_view("/api/v1/packages", v1.PackagesView) + application.router.add_view("/api/v1/packages/{package}", v1.PackageView) + application.router.add_view("/api/v1/packages/{package}/logs", v1.LogsView) + application.router.add_view("/api/v2/packages/{package}/logs", v2.LogsView) - application.router.add_view("/api/v1/status", StatusView) + application.router.add_view("/api/v1/status", v1.StatusView) - application.router.add_view("/api/v1/login", LoginView) - application.router.add_view("/api/v1/logout", LogoutView) + application.router.add_view("/api/v1/login", v1.LoginView) + application.router.add_view("/api/v1/logout", v1.LogoutView) diff --git a/src/ahriman/web/schemas/__init__.py b/src/ahriman/web/schemas/__init__.py index 5253204b..f6e582a8 100644 --- a/src/ahriman/web/schemas/__init__.py +++ b/src/ahriman/web/schemas/__init__.py @@ -25,13 +25,14 @@ from ahriman.web.schemas.file_schema import FileSchema from ahriman.web.schemas.internal_status_schema import InternalStatusSchema from ahriman.web.schemas.log_schema import LogSchema from ahriman.web.schemas.login_schema import LoginSchema -from ahriman.web.schemas.logs_schema import LogsSchema +from ahriman.web.schemas.logs_schema import LogsSchema, LogsSchemaV2 from ahriman.web.schemas.oauth2_schema import OAuth2Schema from ahriman.web.schemas.package_name_schema import PackageNameSchema from ahriman.web.schemas.package_names_schema import PackageNamesSchema from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema from ahriman.web.schemas.package_schema import PackageSchema from ahriman.web.schemas.package_status_schema import PackageStatusSimplifiedSchema, PackageStatusSchema +from ahriman.web.schemas.pagination_schema import PaginationSchema from ahriman.web.schemas.pgp_key_id_schema import PGPKeyIdSchema from ahriman.web.schemas.pgp_key_schema import PGPKeySchema from ahriman.web.schemas.process_id_schema import ProcessIdSchema diff --git a/src/ahriman/web/schemas/logs_schema.py b/src/ahriman/web/schemas/logs_schema.py index 6ef64180..35bbc7b4 100644 --- a/src/ahriman/web/schemas/logs_schema.py +++ b/src/ahriman/web/schemas/logs_schema.py @@ -37,3 +37,21 @@ class LogsSchema(Schema): logs = fields.String(required=True, metadata={ "description": "Full package log from the last build", }) + + +class LogsSchemaV2(Schema): + """ + response package logs api v2 schema + """ + + package_base = fields.String(required=True, metadata={ + "description": "Package base name", + "example": "ahriman", + }) + status = fields.Nested(StatusSchema(), required=True, metadata={ + "description": "Last package status", + }) + logs = fields.List(fields.Tuple([fields.Float(), fields.String()]), required=True, metadata={ # type: ignore[no-untyped-call] + "description": "Package log records timestamp and message", + "example": [(1680537091.233495, "log record")] + }) diff --git a/src/ahriman/web/schemas/pagination_schema.py b/src/ahriman/web/schemas/pagination_schema.py new file mode 100644 index 00000000..a3ee142b --- /dev/null +++ b/src/ahriman/web/schemas/pagination_schema.py @@ -0,0 +1,35 @@ +# +# Copyright (c) 2021-2023 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, fields + + +class PaginationSchema(Schema): + """ + request pagination schema + """ + + limit = fields.Integer(metadata={ + "description": "Limit records by specified amount", + "example": 42, + }) + offset = fields.Integer(metadata={ + "description": "Start records with the offset", + "example": 100, + }) diff --git a/src/ahriman/web/views/v1/__init__.py b/src/ahriman/web/views/v1/__init__.py new file mode 100644 index 00000000..a7057660 --- /dev/null +++ b/src/ahriman/web/views/v1/__init__.py @@ -0,0 +1,36 @@ +# +# Copyright (c) 2021-2023 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 ahriman.web.views.v1.service.add import AddView +from ahriman.web.views.v1.service.pgp import PGPView +from ahriman.web.views.v1.service.process import ProcessView +from ahriman.web.views.v1.service.rebuild import RebuildView +from ahriman.web.views.v1.service.remove import RemoveView +from ahriman.web.views.v1.service.request import RequestView +from ahriman.web.views.v1.service.search import SearchView +from ahriman.web.views.v1.service.update import UpdateView +from ahriman.web.views.v1.service.upload import UploadView + +from ahriman.web.views.v1.status.logs import LogsView +from ahriman.web.views.v1.status.package import PackageView +from ahriman.web.views.v1.status.packages import PackagesView +from ahriman.web.views.v1.status.status import StatusView + +from ahriman.web.views.v1.user.login import LoginView +from ahriman.web.views.v1.user.logout import LogoutView diff --git a/src/ahriman/web/views/service/__init__.py b/src/ahriman/web/views/v1/service/__init__.py similarity index 100% rename from src/ahriman/web/views/service/__init__.py rename to src/ahriman/web/views/v1/service/__init__.py diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/v1/service/add.py similarity index 100% rename from src/ahriman/web/views/service/add.py rename to src/ahriman/web/views/v1/service/add.py diff --git a/src/ahriman/web/views/service/pgp.py b/src/ahriman/web/views/v1/service/pgp.py similarity index 100% rename from src/ahriman/web/views/service/pgp.py rename to src/ahriman/web/views/v1/service/pgp.py diff --git a/src/ahriman/web/views/service/process.py b/src/ahriman/web/views/v1/service/process.py similarity index 100% rename from src/ahriman/web/views/service/process.py rename to src/ahriman/web/views/v1/service/process.py diff --git a/src/ahriman/web/views/service/rebuild.py b/src/ahriman/web/views/v1/service/rebuild.py similarity index 100% rename from src/ahriman/web/views/service/rebuild.py rename to src/ahriman/web/views/v1/service/rebuild.py diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/v1/service/remove.py similarity index 100% rename from src/ahriman/web/views/service/remove.py rename to src/ahriman/web/views/v1/service/remove.py diff --git a/src/ahriman/web/views/service/request.py b/src/ahriman/web/views/v1/service/request.py similarity index 100% rename from src/ahriman/web/views/service/request.py rename to src/ahriman/web/views/v1/service/request.py diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/v1/service/search.py similarity index 100% rename from src/ahriman/web/views/service/search.py rename to src/ahriman/web/views/v1/service/search.py diff --git a/src/ahriman/web/views/service/update.py b/src/ahriman/web/views/v1/service/update.py similarity index 100% rename from src/ahriman/web/views/service/update.py rename to src/ahriman/web/views/v1/service/update.py diff --git a/src/ahriman/web/views/service/upload.py b/src/ahriman/web/views/v1/service/upload.py similarity index 100% rename from src/ahriman/web/views/service/upload.py rename to src/ahriman/web/views/v1/service/upload.py diff --git a/src/ahriman/web/views/status/__init__.py b/src/ahriman/web/views/v1/status/__init__.py similarity index 100% rename from src/ahriman/web/views/status/__init__.py rename to src/ahriman/web/views/v1/status/__init__.py diff --git a/src/ahriman/web/views/status/logs.py b/src/ahriman/web/views/v1/status/logs.py similarity index 97% rename from src/ahriman/web/views/status/logs.py rename to src/ahriman/web/views/v1/status/logs.py index 4659ae88..2ab07fcb 100644 --- a/src/ahriman/web/views/status/logs.py +++ b/src/ahriman/web/views/v1/status/logs.py @@ -22,6 +22,7 @@ import aiohttp_apispec # type: ignore[import] from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from ahriman.core.exceptions import UnknownPackageError +from ahriman.core.util 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, LogSchema, LogsSchema, PackageNameSchema @@ -103,7 +104,7 @@ class LogsView(BaseView): response = { "package_base": package_base, "status": status.view(), - "logs": logs + "logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for created, message in logs) } return json_response(response) diff --git a/src/ahriman/web/views/status/package.py b/src/ahriman/web/views/v1/status/package.py similarity index 100% rename from src/ahriman/web/views/status/package.py rename to src/ahriman/web/views/v1/status/package.py diff --git a/src/ahriman/web/views/status/packages.py b/src/ahriman/web/views/v1/status/packages.py similarity index 100% rename from src/ahriman/web/views/status/packages.py rename to src/ahriman/web/views/v1/status/packages.py diff --git a/src/ahriman/web/views/status/status.py b/src/ahriman/web/views/v1/status/status.py similarity index 100% rename from src/ahriman/web/views/status/status.py rename to src/ahriman/web/views/v1/status/status.py diff --git a/src/ahriman/web/views/user/__init__.py b/src/ahriman/web/views/v1/user/__init__.py similarity index 100% rename from src/ahriman/web/views/user/__init__.py rename to src/ahriman/web/views/v1/user/__init__.py diff --git a/src/ahriman/web/views/user/login.py b/src/ahriman/web/views/v1/user/login.py similarity index 100% rename from src/ahriman/web/views/user/login.py rename to src/ahriman/web/views/v1/user/login.py diff --git a/src/ahriman/web/views/user/logout.py b/src/ahriman/web/views/v1/user/logout.py similarity index 100% rename from src/ahriman/web/views/user/logout.py rename to src/ahriman/web/views/v1/user/logout.py diff --git a/src/ahriman/web/views/v2/__init__.py b/src/ahriman/web/views/v2/__init__.py new file mode 100644 index 00000000..c67945b4 --- /dev/null +++ b/src/ahriman/web/views/v2/__init__.py @@ -0,0 +1,20 @@ +# +# Copyright (c) 2021-2023 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 ahriman.web.views.v2.status.logs import LogsView diff --git a/src/ahriman/web/views/v2/status/__init__.py b/src/ahriman/web/views/v2/status/__init__.py new file mode 100644 index 00000000..8fc622e9 --- /dev/null +++ b/src/ahriman/web/views/v2/status/__init__.py @@ -0,0 +1,19 @@ +# +# Copyright (c) 2021-2023 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 . +# diff --git a/src/ahriman/web/views/v2/status/logs.py b/src/ahriman/web/views/v2/status/logs.py new file mode 100644 index 00000000..505f56d8 --- /dev/null +++ b/src/ahriman/web/views/v2/status/logs.py @@ -0,0 +1,87 @@ +# +# Copyright (c) 2021-2023 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 . +# +import aiohttp_apispec # type: ignore[import] + +from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response + +from ahriman.core.exceptions import UnknownPackageError +from ahriman.models.user_access import UserAccess +from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PaginationSchema +from ahriman.web.schemas.logs_schema import LogsSchemaV2 +from ahriman.web.views.base import BaseView + + +class LogsView(BaseView): + """ + package logs web view + + Attributes: + GET_PERMISSION(UserAccess): (class attribute) get permissions of self + """ + + GET_PERMISSION = UserAccess.Reporter + + @aiohttp_apispec.docs( + tags=["Packages"], + summary="Get paginated package logs", + description="Retrieve package logs and the last package status", + responses={ + 200: {"description": "Success response", "schema": LogsSchemaV2}, + 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]}], + ) + @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 + + Returns: + Response: 200 with package logs on success + + Raises: + HTTPBadRequest: if supplied parameters are invalid + HTTPNotFound: if package base is unknown + """ + package_base = self.request.match_info["package"] + try: + limit = int(self.request.query.getone("limit", default=-1)) + offset = int(self.request.query.getone("offset", default=0)) + except Exception as ex: + raise HTTPBadRequest(reason=str(ex)) + + try: + _, status = self.service.package_get(package_base) + except UnknownPackageError: + raise HTTPNotFound + logs = self.service.logs_get(package_base, limit, offset) + + response = { + "package_base": package_base, + "status": status.view(), + "logs": logs, + } + return json_response(response) diff --git a/tests/ahriman/core/database/operations/test_logs_operations.py b/tests/ahriman/core/database/operations/test_logs_operations.py index ee0a6922..c4802be7 100644 --- a/tests/ahriman/core/database/operations/test_logs_operations.py +++ b/tests/ahriman/core/database/operations/test_logs_operations.py @@ -13,8 +13,8 @@ def test_logs_insert_remove_process(database: SQLite, package_ahriman: Package, database.logs_insert(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3") database.logs_remove(package_ahriman.base, "1") - assert database.logs_get(package_ahriman.base) == "[1970-01-01 00:00:42] message 1" - assert database.logs_get(package_python_schedule.base) == "[1970-01-01 00:00:42] message 3" + assert database.logs_get(package_ahriman.base) == [(42.0, "message 1")] + assert database.logs_get(package_python_schedule.base) == [(42.0, "message 3")] def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None: @@ -27,7 +27,7 @@ def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, pac database.logs_remove(package_ahriman.base, None) assert not database.logs_get(package_ahriman.base) - assert database.logs_get(package_python_schedule.base) == "[1970-01-01 00:00:42] message 3" + assert database.logs_get(package_python_schedule.base) == [(42.0, "message 3")] def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None: @@ -36,4 +36,13 @@ def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None: """ database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2") database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1") - assert database.logs_get(package_ahriman.base) == "[1970-01-01 00:00:42] message 1\n[1970-01-01 00:00:43] message 2" + assert database.logs_get(package_ahriman.base) == [(42.0, "message 1"), (43.0, "message 2")] + + +def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package) -> None: + """ + must insert and get package logs with pagination + """ + database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1") + database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2") + assert database.logs_get(package_ahriman.base, 1, 1) == [(43.0, "message 2")] diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py index 0053d29d..2fb618c8 100644 --- a/tests/ahriman/core/status/test_watcher.py +++ b/tests/ahriman/core/status/test_watcher.py @@ -55,8 +55,8 @@ def test_logs_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixt must return package logs """ logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_get") - watcher.logs_get(package_ahriman.base) - logs_mock.assert_called_once_with(package_ahriman.base) + watcher.logs_get(package_ahriman.base, 1, 2) + logs_mock.assert_called_once_with(package_ahriman.base, 1, 2) def test_logs_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: diff --git a/tests/ahriman/test_tests.py b/tests/ahriman/test_tests.py new file mode 100644 index 00000000..e6c349ee --- /dev/null +++ b/tests/ahriman/test_tests.py @@ -0,0 +1,27 @@ +from pathlib import Path + +from ahriman.core.util import walk + + +def test_test_coverage() -> None: + """ + must have test files for each source file + """ + root = Path() + for source_file in filter(lambda fn: fn.suffix == ".py" and fn.name != "__init__.py", walk(root / "src")): + # some workaround for well known files + if source_file.parts[2:4] == ("application", "handlers") and source_file.name != "handler.py": + filename = f"test_handler_{source_file.name}" + elif source_file.parts[2:4] == ("web", "views"): + if (api := source_file.parts[4]) == "api": + filename = f"test_view_{api}_{source_file.name}" + elif (version := source_file.parts[4]) in ("v1", "v2"): + api = source_file.parts[5] + filename = f"test_view_{version}_{api}_{source_file.name}" + else: + filename = f"test_view_{source_file.name}" + else: + filename = f"test_{source_file.name}" + + test_file = Path("tests", *source_file.parts[1:-1], filename) + assert test_file.is_file(), test_file diff --git a/tests/ahriman/web/schemas/test_pagination_schema.py b/tests/ahriman/web/schemas/test_pagination_schema.py new file mode 100644 index 00000000..1982fb6b --- /dev/null +++ b/tests/ahriman/web/schemas/test_pagination_schema.py @@ -0,0 +1 @@ +# schema testing goes in view class tests diff --git a/tests/ahriman/web/views/api/test_views_api_docs.py b/tests/ahriman/web/views/api/test_view_api_docs.py similarity index 100% rename from tests/ahriman/web/views/api/test_views_api_docs.py rename to tests/ahriman/web/views/api/test_view_api_docs.py diff --git a/tests/ahriman/web/views/api/test_views_api_swagger.py b/tests/ahriman/web/views/api/test_view_api_swagger.py similarity index 100% rename from tests/ahriman/web/views/api/test_views_api_swagger.py rename to tests/ahriman/web/views/api/test_view_api_swagger.py diff --git a/tests/ahriman/web/views/test_views_base.py b/tests/ahriman/web/views/test_view_base.py similarity index 100% rename from tests/ahriman/web/views/test_views_base.py rename to tests/ahriman/web/views/test_view_base.py diff --git a/tests/ahriman/web/views/test_views_index.py b/tests/ahriman/web/views/test_view_index.py similarity index 100% rename from tests/ahriman/web/views/test_views_index.py rename to tests/ahriman/web/views/test_view_index.py diff --git a/tests/ahriman/web/views/service/test_views_service_add.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_add.py similarity index 97% rename from tests/ahriman/web/views/service/test_views_service_add.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_add.py index d9a145c2..f6948906 100644 --- a/tests/ahriman/web/views/service/test_views_service_add.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_add.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.add import AddView +from ahriman.web.views.v1 import AddView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_pgp.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py similarity index 98% rename from tests/ahriman/web/views/service/test_views_service_pgp.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py index 3dd13eae..9cb95dbb 100644 --- a/tests/ahriman/web/views/service/test_views_service_pgp.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_pgp.py @@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.pgp import PGPView +from ahriman.web.views.v1 import PGPView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_process.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_process.py similarity index 96% rename from tests/ahriman/web/views/service/test_views_service_process.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_process.py index e9adf12d..1fa16cae 100644 --- a/tests/ahriman/web/views/service/test_views_service_process.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_process.py @@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.process import ProcessView +from ahriman.web.views.v1 import ProcessView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_rebuild.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py similarity index 96% rename from tests/ahriman/web/views/service/test_views_service_rebuild.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py index 286906c3..66bf475d 100644 --- a/tests/ahriman/web/views/service/test_views_service_rebuild.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_rebuild.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.rebuild import RebuildView +from ahriman.web.views.v1 import RebuildView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_remove.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py similarity index 96% rename from tests/ahriman/web/views/service/test_views_service_remove.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py index 46398542..dc1feaf0 100644 --- a/tests/ahriman/web/views/service/test_views_service_remove.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_remove.py @@ -4,7 +4,7 @@ from aiohttp.test_utils import TestClient from pytest_mock import MockerFixture from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.remove import RemoveView +from ahriman.web.views.v1 import RemoveView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_request.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_request.py similarity index 96% rename from tests/ahriman/web/views/service/test_views_service_request.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_request.py index 2f1e656d..4966eecf 100644 --- a/tests/ahriman/web/views/service/test_views_service_request.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_request.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.request import RequestView +from ahriman.web.views.v1 import RequestView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_search.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_search.py similarity index 98% rename from tests/ahriman/web/views/service/test_views_service_search.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_search.py index 2ce84e20..533f8fc0 100644 --- a/tests/ahriman/web/views/service/test_views_service_search.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_search.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from ahriman.models.aur_package import AURPackage from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.search import SearchView +from ahriman.web.views.v1 import SearchView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_update.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_update.py similarity index 97% rename from tests/ahriman/web/views/service/test_views_service_update.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_update.py index d60a9cd7..d29f4b9d 100644 --- a/tests/ahriman/web/views/service/test_views_service_update.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_update.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from unittest.mock import AsyncMock from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.update import UpdateView +from ahriman.web.views.v1 import UpdateView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/service/test_views_service_upload.py b/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py similarity index 95% rename from tests/ahriman/web/views/service/test_views_service_upload.py rename to tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py index 87d36368..a472651a 100644 --- a/tests/ahriman/web/views/service/test_views_service_upload.py +++ b/tests/ahriman/web/views/v1/service/test_view_v1_service_upload.py @@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, call as MockCall from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.user_access import UserAccess -from ahriman.web.views.service.upload import UploadView +from ahriman.web.views.v1 import UploadView async def test_get_permission() -> None: @@ -30,7 +30,7 @@ async def test_save_file(mocker: MockerFixture) -> None: part_mock.filename = "filename" part_mock.read_chunk = AsyncMock(side_effect=[b"content", None]) - tempfile_mock = mocker.patch("ahriman.web.views.service.upload.NamedTemporaryFile") + tempfile_mock = mocker.patch("ahriman.web.views.v1.service.upload.NamedTemporaryFile") file_mock = MagicMock() tempfile_mock.return_value.__enter__.return_value = file_mock @@ -84,7 +84,7 @@ async def test_post(client: TestClient, repository_paths: RepositoryPaths, mocke must process file upload via http """ local = Path("local") - save_mock = mocker.patch("ahriman.web.views.service.upload.UploadView.save_file", + save_mock = mocker.patch("ahriman.web.views.v1.UploadView.save_file", side_effect=AsyncMock(return_value=("filename", local / ".filename"))) rename_mock = mocker.patch("pathlib.Path.rename") # no content validation here because it has invalid schema @@ -103,7 +103,7 @@ async def test_post_with_sig(client: TestClient, repository_paths: RepositoryPat must process file upload with signature via http """ local = Path("local") - save_mock = mocker.patch("ahriman.web.views.service.upload.UploadView.save_file", + save_mock = mocker.patch("ahriman.web.views.v1.UploadView.save_file", side_effect=AsyncMock(side_effect=[ ("filename", local / ".filename"), ("filename.sig", local / ".filename.sig"), diff --git a/tests/ahriman/web/views/status/test_views_status_logs.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py similarity index 98% rename from tests/ahriman/web/views/status/test_views_status_logs.py rename to tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py index af36ec39..1a73a010 100644 --- a/tests/ahriman/web/views/status/test_views_status_logs.py +++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_logs.py @@ -5,7 +5,7 @@ from aiohttp.test_utils import TestClient from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package import Package from ahriman.models.user_access import UserAccess -from ahriman.web.views.status.logs import LogsView +from ahriman.web.views.v1 import LogsView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/status/test_views_status_package.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_package.py similarity index 99% rename from tests/ahriman/web/views/status/test_views_status_package.py rename to tests/ahriman/web/views/v1/status/test_view_v1_status_package.py index 63f94a73..8559f7fd 100644 --- a/tests/ahriman/web/views/status/test_views_status_package.py +++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_package.py @@ -5,7 +5,7 @@ from aiohttp.test_utils import TestClient from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package from ahriman.models.user_access import UserAccess -from ahriman.web.views.status.package import PackageView +from ahriman.web.views.v1 import PackageView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/status/test_views_status_packages.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py similarity index 97% rename from tests/ahriman/web/views/status/test_views_status_packages.py rename to tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py index 8969a78d..a3d4b982 100644 --- a/tests/ahriman/web/views/status/test_views_status_packages.py +++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_packages.py @@ -6,7 +6,7 @@ from pytest_mock import MockerFixture from ahriman.models.build_status import BuildStatusEnum from ahriman.models.package import Package from ahriman.models.user_access import UserAccess -from ahriman.web.views.status.packages import PackagesView +from ahriman.web.views.v1 import PackagesView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/status/test_views_status_status.py b/tests/ahriman/web/views/v1/status/test_view_v1_status_status.py similarity index 98% rename from tests/ahriman/web/views/status/test_views_status_status.py rename to tests/ahriman/web/views/v1/status/test_view_v1_status_status.py index 8902875a..fbb1035d 100644 --- a/tests/ahriman/web/views/status/test_views_status_status.py +++ b/tests/ahriman/web/views/v1/status/test_view_v1_status_status.py @@ -8,7 +8,7 @@ from ahriman.models.build_status import BuildStatusEnum from ahriman.models.internal_status import InternalStatus from ahriman.models.package import Package from ahriman.models.user_access import UserAccess -from ahriman.web.views.status.status import StatusView +from ahriman.web.views.v1 import StatusView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/user/test_views_user_login.py b/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py similarity index 99% rename from tests/ahriman/web/views/user/test_views_user_login.py rename to tests/ahriman/web/views/v1/user/test_view_v1_user_login.py index d1e7eacb..c8f35f67 100644 --- a/tests/ahriman/web/views/user/test_views_user_login.py +++ b/tests/ahriman/web/views/v1/user/test_view_v1_user_login.py @@ -5,7 +5,7 @@ from pytest_mock import MockerFixture from ahriman.models.user import User from ahriman.models.user_access import UserAccess -from ahriman.web.views.user.login import LoginView +from ahriman.web.views.v1 import LoginView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/user/test_views_user_logout.py b/tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py similarity index 96% rename from tests/ahriman/web/views/user/test_views_user_logout.py rename to tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py index 3dccec0d..651a38a6 100644 --- a/tests/ahriman/web/views/user/test_views_user_logout.py +++ b/tests/ahriman/web/views/v1/user/test_view_v1_user_logout.py @@ -5,7 +5,7 @@ from aiohttp.web import HTTPUnauthorized from pytest_mock import MockerFixture from ahriman.models.user_access import UserAccess -from ahriman.web.views.user.logout import LogoutView +from ahriman.web.views.v1 import LogoutView async def test_get_permission() -> None: diff --git a/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py b/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py new file mode 100644 index 00000000..c9a31ef7 --- /dev/null +++ b/tests/ahriman/web/views/v2/status/test_view_v2_status_logs.py @@ -0,0 +1,93 @@ +import pytest + +from aiohttp.test_utils import TestClient + +from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.package import Package +from ahriman.models.user_access import UserAccess +from ahriman.web.views.v2 import LogsView + + +async def test_get_permission() -> None: + """ + must return correct permission for the request + """ + for method in ("GET",): + request = pytest.helpers.request("", "", method) + assert await LogsView.get_permission(request) == UserAccess.Reporter + + +async def test_get(client: TestClient, package_ahriman: Package) -> None: + """ + must get logs for package + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 42.0, "message": "message 1", "version": "42"}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 43.0, "message": "message 2", "version": "42"}) + request_schema = pytest.helpers.schema_request(LogsView.get, location="querystring") + response_schema = pytest.helpers.schema_response(LogsView.get) + + payload = {} + assert not request_schema.validate(payload) + response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params=payload) + assert response.status == 200 + + logs = await response.json() + assert not response_schema.validate(logs) + assert logs["logs"] == [[42.0, "message 1"], [43.0, "message 2"]] + + +async def test_get_with_pagination(client: TestClient, package_ahriman: Package) -> None: + """ + must get logs with pagination + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 42.0, "message": "message 1", "version": "42"}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 43.0, "message": "message 2", "version": "42"}) + request_schema = pytest.helpers.schema_request(LogsView.get, location="querystring") + response_schema = pytest.helpers.schema_response(LogsView.get) + + payload = {"limit": 1, "offset": 1} + assert not request_schema.validate(payload) + response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params=payload) + assert response.status == 200 + + logs = await response.json() + assert not response_schema.validate(logs) + assert logs["logs"] == [[43.0, "message 2"]] + + +async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None: + """ + must return not found for missing package + """ + response_schema = pytest.helpers.schema_response(LogsView.get, code=404) + + response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs") + assert response.status == 404 + assert not response_schema.validate(await response.json()) + + +async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None: + """ + must return bad request for invalid query parameters + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 42.0, "message": "message", "version": "42"}) + response_schema = pytest.helpers.schema_response(LogsView.get, code=400) + + response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params={"limit": "limit"}) + assert response.status == 400 + assert not response_schema.validate(await response.json()) + + response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params={"offset": "offset"}) + assert response.status == 400 + assert not response_schema.validate(await response.json())