feat: pagination support for logs request

This commit is contained in:
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

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