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())