mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-16 07:19:57 +00:00
feat: pagination support for logs request
This commit is contained in:
@ -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:
|
||||
"""
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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")]
|
||||
})
|
||||
|
35
src/ahriman/web/schemas/pagination_schema.py
Normal file
35
src/ahriman/web/schemas/pagination_schema.py
Normal 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,
|
||||
})
|
36
src/ahriman/web/views/v1/__init__.py
Normal file
36
src/ahriman/web/views/v1/__init__.py
Normal 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
|
@ -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)
|
||||
|
20
src/ahriman/web/views/v2/__init__.py
Normal file
20
src/ahriman/web/views/v2/__init__.py
Normal 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
|
19
src/ahriman/web/views/v2/status/__init__.py
Normal file
19
src/ahriman/web/views/v2/status/__init__.py
Normal 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/>.
|
||||
#
|
87
src/ahriman/web/views/v2/status/logs.py
Normal file
87
src/ahriman/web/views/v2/status/logs.py
Normal 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)
|
Reference in New Issue
Block a user