feat: allow to use single web instance for all repositories (#114)

* Allow to use single web instance for any repository

* some improvements

* drop includes from user home directory, introduce new variables to docker

The old solution didn't actually work as expected, because devtools
configuration belongs to filesystem (as well as sudo one), so it was
still required to run setup command.

In order to handle additional repositories, the POSTSETUP and PRESETUP
commands variables have been introduced. FAQ has been updated as well

* raise 404 in case if repository is unknown
This commit is contained in:
2023-10-17 03:53:33 +03:00
parent bf9a46936c
commit 1e00bf9398
141 changed files with 2037 additions and 917 deletions

View File

@@ -18,14 +18,16 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp_cors import CorsViewMixin # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Request, StreamResponse, View
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
from collections.abc import Awaitable, Callable
from typing import Any, TypeVar
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
@@ -56,15 +58,25 @@ class BaseView(View, CorsViewMixin):
return configuration
@property
def service(self) -> Watcher:
def services(self) -> dict[RepositoryId, Watcher]:
"""
get status watcher instance
get all loaded watchers
Returns:
Watcher: build status watcher instance
dict[RepositoryId, Watcher]: map of loaded watchers per known repository
"""
watcher: Watcher = self.request.app["watcher"]
return watcher
watchers: dict[RepositoryId, Watcher] = self.request.app["watcher"]
return watchers
@property
def sign(self) -> GPG:
"""
get GPG control instance
Returns:
GPG: GPG wrapper instance
"""
return GPG(self.configuration)
@property
def spawner(self) -> Spawn:
@@ -197,8 +209,8 @@ class BaseView(View, CorsViewMixin):
HTTPBadRequest: if supplied parameters are invalid
"""
try:
limit = int(self.request.query.getone("limit", default=-1))
offset = int(self.request.query.getone("offset", default=0))
limit = int(self.request.query.get("limit", default=-1))
offset = int(self.request.query.get("offset", default=0))
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
@@ -210,6 +222,40 @@ class BaseView(View, CorsViewMixin):
return limit, offset
def repository_id(self) -> RepositoryId:
"""
extract repository from request
Returns:
RepositoryIde: repository if possible to construct and first one otherwise
"""
architecture = self.request.query.get("architecture")
name = self.request.query.get("repository")
if architecture and name:
return RepositoryId(architecture, name)
return next(iter(sorted(self.services.keys())))
def service(self, repository_id: RepositoryId | None = None) -> Watcher:
"""
get status watcher instance
Args:
repository_id(RepositoryId | None, optional): repository unique identifier (Default value = None)
Returns:
Watcher: build status watcher instance. If no repository provided, it will return the first one
Raises:
HTTPNotFound: if no repository found
"""
if repository_id is None:
repository_id = self.repository_id()
try:
return self.services[repository_id]
except KeyError:
raise HTTPNotFound(reason=f"Repository {repository_id.id} is unknown")
async def username(self) -> str | None:
"""
extract username from request if any

View File

@@ -37,7 +37,10 @@ class IndexView(BaseView):
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
* index_url - url to the repository index, string, optional
* repository - repository name, string, required
* repositories - list of repositories unique identifiers, required
* id - unique repository identifier, string, required
* repository - repository name, string, required
* architecture - repository architecture, string, required
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
@@ -64,5 +67,11 @@ class IndexView(BaseView):
return {
"auth": auth,
"index_url": self.configuration.get("web", "index_url", fallback=None),
"repository": self.service.repository.name,
"repositories": [
{
"id": repository.id,
**repository.view(),
}
for repository in sorted(self.services)
]
}

View File

@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -46,11 +46,13 @@ class AddView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@@ -68,7 +70,8 @@ class AddView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_add(packages, username, now=True)
process_id = self.spawner.packages_add(repository_id, packages, username, now=True)
return json_response({"process_id": process_id})

View File

@@ -48,7 +48,7 @@ class PGPView(BaseView):
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},
404: {"description": "PGP key is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
@@ -67,15 +67,15 @@ class PGPView(BaseView):
HTTPNotFound: if key wasn't found or service was unable to fetch it
"""
try:
key = self.get_non_empty(self.request.query.getone, "key")
server = self.get_non_empty(self.request.query.getone, "server")
key = self.get_non_empty(self.request.query.get, "key")
server = self.get_non_empty(self.request.query.get, "server")
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
try:
key = self.service.repository.sign.key_download(server, key)
key = self.sign.key_download(server, key)
except Exception:
raise HTTPNotFound
raise HTTPNotFound(reason=f"Key {key} is unknown")
return json_response({"key": key})

View File

@@ -45,7 +45,7 @@ class ProcessView(BaseView):
200: {"description": "Success response", "schema": ProcessSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Not found", "schema": ErrorSchema},
404: {"description": "Process is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],

View File

@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -46,11 +46,13 @@ class RebuildView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@@ -69,7 +71,8 @@ class RebuildView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_rebuild(depends_on, username)
process_id = self.spawner.packages_rebuild(repository_id, depends_on, username)
return json_response({"process_id": process_id})

View File

@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -46,11 +46,13 @@ class RemoveView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@@ -68,6 +70,7 @@ class RemoveView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
process_id = self.spawner.packages_remove(packages)
repository_id = self.repository_id()
process_id = self.spawner.packages_remove(repository_id, packages)
return json_response({"process_id": process_id})

View File

@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -46,11 +46,13 @@ class RequestView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@@ -69,6 +71,7 @@ class RequestView(BaseView):
raise HTTPBadRequest(reason=str(ex))
username = await self.username()
process_id = self.spawner.packages_add(packages, username, now=False)
repository_id = self.repository_id()
process_id = self.spawner.packages_add(repository_id, packages, username, now=False)
return json_response({"process_id": process_id})

View File

@@ -69,7 +69,7 @@ class SearchView(BaseView):
"""
try:
search: list[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
packages = AUR.multisearch(*search)
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))

View File

@@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, UpdateFlagsSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, UpdateFlagsSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -46,11 +46,13 @@ class UpdateView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(UpdateFlagsSchema)
async def post(self) -> Response:
"""
@@ -67,8 +69,10 @@ class UpdateView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_update(
repository_id,
username,
aur=data.get("aur", True),
local=data.get("local", True),

View File

@@ -25,8 +25,9 @@ from aiohttp.web import HTTPBadRequest, HTTPCreated, HTTPNotFound
from pathlib import Path
from tempfile import NamedTemporaryFile
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -100,12 +101,13 @@ class UploadView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Not found", "schema": ErrorSchema},
404: {"description": "Repository is unknown or endpoint is disabled", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.form_schema(FileSchema)
async def post(self) -> None:
"""
@@ -125,7 +127,10 @@ class UploadView(BaseView):
raise HTTPBadRequest(reason=str(ex))
max_body_size = self.configuration.getint("web", "max_body_size", fallback=None)
target = self.configuration.repository_paths.packages
paths_root = self.configuration.repository_paths.root
repository_id = self.repository_id()
target = RepositoryPaths(paths_root, repository_id).packages
files = []
while (part := await reader.next()) is not None:

View File

@@ -25,7 +25,7 @@ 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
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -51,12 +51,14 @@ class LogsView(BaseView):
204: {"description": "Success response"},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [DELETE_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def delete(self) -> None:
"""
delete package logs
@@ -65,7 +67,7 @@ class LogsView(BaseView):
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service.logs_remove(package_base, None)
self.service().logs_remove(package_base, None)
raise HTTPNoContent
@@ -77,13 +79,14 @@ class LogsView(BaseView):
200: {"description": "Success response", "schema": LogsSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Package base is unknown", "schema": ErrorSchema},
404: {"description": "Package base and/or repository are 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(RepositoryIdSchema)
async def get(self) -> Response:
"""
get last package logs
@@ -97,10 +100,10 @@ class LogsView(BaseView):
package_base = self.request.match_info["package"]
try:
_, status = self.service.package_get(package_base)
_, status = self.service().package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound
logs = self.service.logs_get(package_base)
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
logs = self.service().logs_get(package_base)
response = {
"package_base": package_base,
@@ -118,6 +121,7 @@ class LogsView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
@@ -143,6 +147,6 @@ class LogsView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service.logs_update(LogRecordId(package_base, version), created, record)
self.service().logs_update(LogRecordId(package_base, version), created, record)
raise HTTPNoContent

View File

@@ -25,7 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, PackageStatusSimplifiedSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, \
PackageStatusSimplifiedSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -51,12 +52,14 @@ class PackageView(BaseView):
204: {"description": "Success response"},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [DELETE_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def delete(self) -> None:
"""
delete package base from status page
@@ -65,7 +68,7 @@ class PackageView(BaseView):
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service.package_remove(package_base)
self.service().package_remove(package_base)
raise HTTPNoContent
@@ -77,13 +80,14 @@ class PackageView(BaseView):
200: {"description": "Success response", "schema": PackageStatusSchema(many=True)},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Package base is unknown", "schema": ErrorSchema},
404: {"description": "Package base and/or repository are 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(RepositoryIdSchema)
async def get(self) -> Response:
"""
get current package base status
@@ -95,16 +99,18 @@ class PackageView(BaseView):
HTTPNotFound: if no package was found
"""
package_base = self.request.match_info["package"]
repository_id = self.repository_id()
try:
package, status = self.service.package_get(package_base)
package, status = self.service(repository_id).package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
response = [
{
"package": package.view(),
"status": status.view()
"status": status.view(),
"repository": repository_id.view(),
}
]
return json_response(response)
@@ -118,12 +124,14 @@ class PackageView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageStatusSimplifiedSchema)
async def post(self) -> None:
"""
@@ -143,7 +151,7 @@ class PackageView(BaseView):
raise HTTPBadRequest(reason=str(ex))
try:
self.service.package_update(package_base, status, package)
self.service().package_update(package_base, status, package)
except UnknownPackageError:
raise HTTPBadRequest(reason=f"Package {package_base} is unknown, but no package body set")

View File

@@ -26,7 +26,7 @@ from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -52,6 +52,7 @@ class PackagesView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
@@ -68,12 +69,16 @@ class PackagesView(BaseView):
limit, offset = self.page()
stop = offset + limit if limit >= 0 else None
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda pair: pair[0].base
repository_id = self.repository_id()
packages = self.service(repository_id).packages
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda items: items[0].base
response = [
{
"package": package.view(),
"status": status.view()
} for package, status in itertools.islice(sorted(self.service.packages, key=comparator), offset, stop)
"status": status.view(),
"repository": repository_id.view(),
} for package, status in itertools.islice(sorted(packages, key=comparator), offset, stop)
]
return json_response(response)
@@ -86,11 +91,13 @@ class PackagesView(BaseView):
204: {"description": "Success response"},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def post(self) -> None:
"""
reload all packages from repository
@@ -98,6 +105,6 @@ class PackagesView(BaseView):
Raises:
HTTPNoContent: on success response
"""
self.service.load()
self.service().load()
raise HTTPNoContent

View File

@@ -0,0 +1,65 @@
#
# 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-untyped]
from aiohttp.web import Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
class RepositoriesView(BaseView):
"""
repositories view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION = UserAccess.Read
ROUTES = ["/api/v1/repositories"]
@aiohttp_apispec.docs(
tags=["Status"],
summary="Available repositories",
description="List available repositories",
responses={
200: {"description": "Success response", "schema": RepositoryIdSchema(many=True)},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
async def get(self) -> Response:
"""
get list of available repositories
Returns:
Response: 200 with service status object
"""
repositories = [
repository_id.view()
for repository_id in sorted(self.services)
]
return json_response(repositories)

View File

@@ -26,7 +26,7 @@ from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@@ -51,6 +51,7 @@ class StatusView(BaseView):
200: {"description": "Success response", "schema": InternalStatusSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
@@ -63,12 +64,13 @@ class StatusView(BaseView):
Returns:
Response: 200 with service status object
"""
counters = Counters.from_packages(self.service.packages)
repository_id = self.repository_id()
counters = Counters.from_packages(self.service(repository_id).packages)
status = InternalStatus(
status=self.service.status,
architecture=self.service.repository_id.architecture,
status=self.service(repository_id).status,
architecture=repository_id.architecture,
packages=counters,
repository=self.service.repository_id.name,
repository=repository_id.name,
version=__version__,
)
@@ -83,11 +85,13 @@ class StatusView(BaseView):
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Repository is unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(StatusSchema)
async def post(self) -> None:
"""
@@ -103,6 +107,6 @@ class StatusView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service.status_update(status)
self.service().status_update(status)
raise HTTPNoContent

View File

@@ -75,7 +75,7 @@ class LoginView(BaseView):
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
code = self.request.query.getone("code", default=None)
code = self.request.query.get("code")
if not code:
raise HTTPFound(oauth_provider.get_oauth_url())

View File

@@ -23,8 +23,7 @@ from aiohttp.web import 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.schemas import AuthSchema, ErrorSchema, LogsSchemaV2, PackageNameSchema, PaginationSchema
from ahriman.web.views.base import BaseView
@@ -48,7 +47,7 @@ class LogsView(BaseView):
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},
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
@@ -70,10 +69,10 @@ class LogsView(BaseView):
limit, offset = self.page()
try:
_, status = self.service.package_get(package_base)
_, status = self.service().package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound
logs = self.service.logs_get(package_base, limit, offset)
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
logs = self.service().logs_get(package_base, limit, offset)
response = {
"package_base": package_base,