feat: pagination support for logs request

This commit is contained in:
Evgenii Alekseev 2023-09-07 18:16:39 +03:00 committed by Evgeniy Alekseev
parent 5e42dd4e70
commit 99eecdebf3
66 changed files with 650 additions and 238 deletions

View File

@ -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.

View File

@ -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
-----------------------------------------------

View File

@ -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
----------

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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");

View File

@ -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:
"""

View File

@ -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:
"""

View File

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

View File

@ -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

View File

@ -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")]
})

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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,
})

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
from ahriman.web.views.v2.status.logs import LogsView

View File

@ -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 <http://www.gnu.org/licenses/>.
#

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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)

View File

@ -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")]

View File

@ -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:

View File

@ -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

View File

@ -0,0 +1 @@
# schema testing goes in view class tests

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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"),

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

@ -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:

View File

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