feat: add patch controls to web, review web, enrich info tab (#115)

* add ability to specify one-time patch on package addition

* support vars in interface
This commit is contained in:
2023-10-29 23:41:20 +02:00
committed by GitHub
parent 8524f1eb20
commit 554827cc57
54 changed files with 1327 additions and 276 deletions

View File

@ -276,6 +276,7 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-s", "--source", help="explicitly specify the package source for this command",
type=PackageSource, choices=enum_values(PackageSource), default=PackageSource.Auto)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.add_argument("-v", "--variable", help="apply specified makepkg variables to the next build", action="append")
parser.set_defaults(handler=handlers.Add)
return parser

View File

@ -23,6 +23,7 @@ from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.packagers import Packagers
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -45,7 +46,12 @@ class Add(Handler):
"""
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
application.on_start()
application.add(args.package, args.source, args.username)
patches = [PkgbuildPatch.from_env(patch) for patch in args.variable] if args.variable is not None else []
for package in args.package: # for each requested package insert patch
application.database.patches_insert(package, patches)
if not args.now:
return

View File

@ -162,7 +162,7 @@ class Handler:
if args.repository_id is not None:
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
# repository parts is optional for backward compatibility
architecture, *repository_parts = args.repository_id.split(separator)
architecture, *repository_parts = args.repository_id.split(separator) # maxsplit isn't used intentionally
args.architecture = architecture
if repository_parts:
args.repository = "-".join(repository_parts) # replace slash with dash

View File

@ -115,7 +115,7 @@ class Patch(Handler):
package_base(str): package base
patch(PkgbuildPatch): patch descriptor
"""
application.database.patches_insert(package_base, patch)
application.database.patches_insert(package_base, [patch])
@staticmethod
def patch_set_list(application: Application, package_base: str | None, variables: list[str] | None,

View File

@ -33,8 +33,8 @@ class PackageOperations(Operations):
package operations
"""
def _package_remove_package_base(self, connection: Connection, package_base: str,
repository_id: RepositoryId) -> None:
@staticmethod
def _package_remove_package_base(connection: Connection, package_base: str, repository_id: RepositoryId) -> None:
"""
remove package base information
@ -50,8 +50,9 @@ class PackageOperations(Operations):
"""delete from package_bases where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": repository_id.id})
def _package_remove_packages(self, connection: Connection, package_base: str,
current_packages: Iterable[str], repository_id: RepositoryId) -> None:
@staticmethod
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str],
repository_id: RepositoryId) -> None:
"""
remove packages belong to the package base
@ -74,8 +75,8 @@ class PackageOperations(Operations):
"""delete from packages where package = :package and repository = :repository""",
packages)
def _package_update_insert_base(self, connection: Connection, package: Package,
repository_id: RepositoryId) -> None:
@staticmethod
def _package_update_insert_base(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
"""
insert base package into table
@ -107,8 +108,8 @@ class PackageOperations(Operations):
}
)
def _package_update_insert_packages(self, connection: Connection, package: Package,
repository_id: RepositoryId) -> None:
@staticmethod
def _package_update_insert_packages(connection: Connection, package: Package, repository_id: RepositoryId) -> None:
"""
insert packages into table
@ -149,7 +150,8 @@ class PackageOperations(Operations):
""",
package_list)
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus,
@staticmethod
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
repository_id: RepositoryId) -> None:
"""
insert base package status into table
@ -176,8 +178,8 @@ class PackageOperations(Operations):
"repository": repository_id.id,
})
def _packages_get_select_package_bases(self, connection: Connection,
repository_id: RepositoryId) -> dict[str, Package]:
@staticmethod
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
"""
select package bases from the table
@ -201,7 +203,8 @@ class PackageOperations(Operations):
)
}
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package],
@staticmethod
def _packages_get_select_packages(connection: Connection, packages: dict[str, Package],
repository_id: RepositoryId) -> dict[str, Package]:
"""
select packages from the table
@ -223,8 +226,8 @@ class PackageOperations(Operations):
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
def _packages_get_select_statuses(self, connection: Connection,
repository_id: RepositoryId) -> dict[str, BuildStatus]:
@staticmethod
def _packages_get_select_statuses(connection: Connection, repository_id: RepositoryId) -> dict[str, BuildStatus]:
"""
select package build statuses from the table

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections import defaultdict
from sqlite3 import Connection
from ahriman.core.database.operations import Operations
@ -42,16 +41,16 @@ class PatchOperations(Operations):
"""
return self.patches_list(package_base, None).get(package_base, [])
def patches_insert(self, package_base: str, patch: PkgbuildPatch) -> None:
def patches_insert(self, package_base: str, patches: list[PkgbuildPatch]) -> None:
"""
insert or update patch in database
Args:
package_base(str): package base to insert
patch(PkgbuildPatch): patch content
patches(list[PkgbuildPatch]): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
connection.executemany(
"""
insert into patches
(package_base, variable, patch)
@ -60,7 +59,14 @@ class PatchOperations(Operations):
on conflict (package_base, coalesce(variable, '')) do update set
patch = :patch
""",
{"package_base": package_base, "variable": patch.key, "patch": patch.value})
[
{
"package_base": package_base,
"variable": patch.key,
"patch": patch.value,
} for patch in patches
]
)
return self.with_connection(run, commit=True)
@ -89,7 +95,7 @@ class PatchOperations(Operations):
if variables is not None and patch.key not in variables:
continue
patches[package].append(patch)
return dict(patches)
return patches
def patches_remove(self, package_base: str, variables: list[str] | None) -> None:
"""
@ -102,12 +108,21 @@ class PatchOperations(Operations):
def run_many(connection: Connection) -> None:
patches = variables or [] # suppress mypy warning
connection.executemany(
"""delete from patches where package_base = :package_base and variable = :variable""",
[{"package_base": package_base, "variable": variable} for variable in patches])
"""
delete from patches where package_base = :package_base and variable = :variable
""",
[
{
"package_base": package_base,
"variable": variable,
} for variable in patches
])
def run(connection: Connection) -> None:
connection.execute(
"""delete from patches where package_base = :package_base""",
"""
delete from patches where package_base = :package_base
""",
{"package_base": package_base})
if variables is not None:

View File

@ -28,6 +28,7 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread
from ahriman.core.log import LazyLogging
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.process_status import ProcessStatus
from ahriman.models.repository_id import RepositoryId
@ -96,7 +97,8 @@ class Spawn(Thread, LazyLogging):
queue.put(ProcessStatus(process_id, result, consumed_time))
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, **kwargs: str | None) -> str:
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str,
**kwargs: str | list[str] | None) -> str:
"""
spawn external ahriman process with supplied arguments
@ -104,7 +106,7 @@ class Spawn(Thread, LazyLogging):
repository_id(RepositoryId): repository unique identifier
command(str): subcommand to run
*args(str): positional command arguments
**kwargs(str): named command arguments
**kwargs(str | list[str] | None): named command arguments
Returns:
str: spawned process identifier
@ -118,9 +120,13 @@ class Spawn(Thread, LazyLogging):
for argument, value in kwargs.items():
if value is None:
continue # skip null values
arguments.append(f"--{argument}")
if value:
arguments.append(value)
flag = f"--{argument}"
if isinstance(value, list):
arguments.extend(list(sum(((flag, v) for v in value), ())))
elif value:
arguments.extend([flag, value])
else:
arguments.append(flag) # boolean argument
process_id = str(uuid.uuid4())
self.logger.info("full command line arguments of %s are %s using repository %s",
@ -167,7 +173,7 @@ class Spawn(Thread, LazyLogging):
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
now: bool) -> str:
patches: list[PkgbuildPatch], now: bool) -> str:
"""
add packages
@ -175,14 +181,18 @@ class Spawn(Thread, LazyLogging):
repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
patches(list[PkgbuildPatch]): list of patches to be passed
now(bool): build packages now
Returns:
str: spawned process identifier
"""
kwargs = {"username": username}
kwargs: dict[str, str | list[str] | None] = {"username": username}
if now:
kwargs["now"] = ""
if patches:
kwargs["variable"] = [patch.serialize() for patch in patches]
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:

View File

@ -23,6 +23,7 @@ from ahriman.core.log import LazyLogging
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_id import RepositoryId
@ -162,6 +163,40 @@ class Watcher(LazyLogging):
self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status, self.repository_id)
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
"""
get patches for the package
Args:
package_base(str): package base
variable(str | None): patch variable name if any
Returns:
list[PkgbuildPatch]: list of patches which are stored for the package
"""
variables = [variable] if variable is not None else None
return self.database.patches_list(package_base, variables).get(package_base, [])
def patches_remove(self, package_base: str, variable: str) -> None:
"""
remove package patch
Args:
package_base(str): package base
variable(str): patch variable name
"""
self.database.patches_remove(package_base, [variable])
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
update package patch
Args:
package_base(str): package base
patch(PkgbuildPatch): package patch
"""
self.database.patches_insert(package_base, [patch])
def status_update(self, status: BuildStatusEnum) -> None:
"""
update service status

View File

@ -56,6 +56,7 @@ __all__ = [
"srcinfo_property",
"srcinfo_property_list",
"trim_package",
"unquote",
"utcnow",
"walk",
]
@ -465,6 +466,38 @@ def trim_package(package_name: str) -> str:
return package_name
def unquote(source: str) -> str:
"""
like ``shlex.quote``, but opposite
Args:
source(str): source string to remove quotes
Returns:
str: string with quotes removed
Raises:
ValueError: if no closing quotation
"""
def generator() -> Generator[str, None, None]:
token = None
for char in source:
if token is not None:
if char == token:
token = None # closed quote
else:
yield char # character inside quotes
elif char in ("'", "\""):
token = char # first quote found
else:
yield char # normal character
if token is not None:
raise ValueError("No closing quotation")
return "".join(generator())
def utcnow() -> datetime.datetime:
"""
get current time

View File

@ -19,8 +19,11 @@
#
import shlex
from dataclasses import dataclass, field
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Self
from ahriman.core.util import dataclass_view, unquote
@dataclass(frozen=True)
@ -33,12 +36,12 @@ class PkgbuildPatch:
considered as full PKGBUILD diffs
value(str | list[str]): value of the stored PKGBUILD property. It must be either string or list of string
values
unsafe(bool): if set, value will be not quoted, might break PKGBUILD
"""
key: str | None
value: str | list[str]
unsafe: bool = field(default=False, kw_only=True)
quote = shlex.quote
def __post_init__(self) -> None:
"""
@ -66,17 +69,26 @@ class PkgbuildPatch:
"""
return self.key is None
def quote(self, value: str) -> str:
@classmethod
def from_env(cls, variable: str) -> Self:
"""
quote value according to the unsafe flag
construct patch from environment variable. Functions are not supported
Args:
value(str): value to be quoted
variable(str): variable in bash form, i.e. KEY=VALUE
Returns:
str: quoted string in case if unsafe is False and as is otherwise
Self: package properties
"""
return value if self.unsafe else shlex.quote(value)
key, *value_parts = variable.split("=", maxsplit=1)
raw_value = next(iter(value_parts), "") # extract raw value
if raw_value.startswith("(") and raw_value.endswith(")"):
value: str | list[str] = shlex.split(raw_value[1:-1]) # arrays for poor
else:
value = unquote(raw_value)
return cls(key, value)
def serialize(self) -> str:
"""
@ -88,14 +100,23 @@ class PkgbuildPatch:
str: serialized key-value pair, print-friendly
"""
if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value))
value = " ".join(map(PkgbuildPatch.quote, self.value))
return f"""{self.key}=({value})"""
if self.is_plain_diff: # no additional logic for plain diffs
return self.value
# we suppose that function values are only supported in string-like values
if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here
return f"""{self.key}={self.quote(self.value)}"""
return f"""{self.key}={PkgbuildPatch.quote(self.value)}"""
def view(self) -> dict[str, Any]:
"""
generate json patch view
Returns:
dict[str, Any]: json-friendly dictionary
"""
return dataclass_view(self)
def write(self, pkgbuild_path: Path) -> None:
"""

View File

@ -52,6 +52,8 @@ class RepositoryId:
Returns:
str: unique id for this repository
"""
if self.is_empty:
return ""
return f"{self.architecture}-{self.name}" # basically the same as used for command line
def query(self) -> list[tuple[str, str]]:

View File

@ -25,14 +25,17 @@ 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, LogsSchemaV2
from ahriman.web.schemas.logs_schema import LogsSchema
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_patch_schema import PackagePatchSchema
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.patch_name_schema import PatchNameSchema
from ahriman.web.schemas.patch_schema import PatchSchema
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
@ -41,4 +44,5 @@ from ahriman.web.schemas.remote_schema import RemoteSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.search_schema import SearchSchema
from ahriman.web.schemas.status_schema import StatusSchema
from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema

View File

@ -17,13 +17,10 @@
# 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 fields
from ahriman import __version__
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from marshmallow import Schema, fields
class LogSchema(RepositoryIdSchema):
class LogSchema(Schema):
"""
request package log schema
"""
@ -32,10 +29,6 @@ class LogSchema(RepositoryIdSchema):
"description": "Log record timestamp",
"example": 1680537091.233495,
})
version = fields.Integer(required=True, metadata={
"description": "Package version to tag",
"example": __version__,
})
message = fields.String(required=True, metadata={
"description": "Log message",
})

View File

@ -27,23 +27,9 @@ class LogsSchema(Schema):
response package logs 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.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",
@ -51,7 +37,3 @@ class LogsSchemaV2(Schema):
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,33 @@
#
# 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 fields
from ahriman.web.schemas.package_names_schema import PackageNamesSchema
from ahriman.web.schemas.patch_schema import PatchSchema
class PackagePatchSchema(PackageNamesSchema):
"""
response schema with packages and patches
"""
patches = fields.Nested(PatchSchema(many=True), metadata={
"description": "optional environment variables to be applied as patches"
})

View File

@ -0,0 +1,33 @@
#
# 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 fields
from ahriman.web.schemas.package_name_schema import PackageNameSchema
class PatchNameSchema(PackageNameSchema):
"""
request package patch schema
"""
patch = fields.String(required=True, metadata={
"description": "Variable name",
"example": "PKGEXT",
})

View File

@ -0,0 +1,33 @@
#
# 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 PatchSchema(Schema):
"""
request and response patch schema
"""
key = fields.String(required=True, metadata={
"description": "environment variable name",
})
value = fields.String(metadata={
"description": "environment variable value",
})

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 fields
from ahriman import __version__
from ahriman.web.schemas.log_schema import LogSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class VersionedLogSchema(LogSchema, RepositoryIdSchema):
"""
request package log schema
"""
version = fields.Integer(required=True, metadata={
"description": "Package version to tag",
"example": __version__,
})

View File

@ -21,8 +21,9 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -53,7 +54,7 @@ class AddView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
@aiohttp_apispec.json_schema(PackagePatchSchema)
async def post(self) -> Response:
"""
add new package
@ -65,13 +66,14 @@ class AddView(BaseView):
HTTPBadRequest: if bad data is supplied
"""
try:
data = await self.extract_data(["packages"])
data = await self.extract_data(["packages", "patches"])
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_add(repository_id, packages, username, now=True)
process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=True)
return json_response({"process_id": process_id})

View File

@ -21,8 +21,9 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackagePatchSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -53,7 +54,7 @@ class RequestView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
@aiohttp_apispec.json_schema(PackagePatchSchema)
async def post(self) -> Response:
"""
request to add new package
@ -65,13 +66,14 @@ class RequestView(BaseView):
HTTPBadRequest: if bad data is supplied
"""
try:
data = await self.extract_data(["packages"])
data = await self.extract_data(["packages", "patches"])
packages = self.get_non_empty(lambda key: [package for package in data[key] if package], "packages")
patches = [PkgbuildPatch(patch["key"], patch.get("value", "")) for patch in data.get("patches", [])]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
username = await self.username()
repository_id = self.repository_id()
process_id = self.spawner.packages_add(repository_id, packages, username, now=False)
process_id = self.spawner.packages_add(repository_id, packages, username, patches=patches, now=False)
return json_response({"process_id": process_id})

View File

@ -25,7 +25,8 @@ 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, RepositoryIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \
VersionedLogSchema
from ahriman.web.views.base import BaseView
@ -128,7 +129,7 @@ class LogsView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.json_schema(LogSchema)
@aiohttp_apispec.json_schema(VersionedLogSchema)
async def post(self) -> None:
"""
create new package log record

View File

@ -0,0 +1,103 @@
#
# 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 HTTPNoContent, HTTPNotFound, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PatchNameSchema, PatchSchema
from ahriman.web.views.base import BaseView
class PatchView(BaseView):
"""
package patch web view
Attributes:
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
DELETE_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/patches/{patch}"]
@aiohttp_apispec.docs(
tags=["Packages"],
summary="Delete package patch",
description="Delete package patch by variable",
responses={
204: {"description": "Success response"},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [DELETE_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PatchNameSchema)
async def delete(self) -> None:
"""
delete package patch
Raises:
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
variable = self.request.match_info["patch"]
self.service().patches_remove(package_base, variable)
raise HTTPNoContent
@aiohttp_apispec.docs(
tags=["Packages"],
summary="Get package patch",
description="Retrieve package patch by variable",
responses={
200: {"description": "Success response", "schema": PatchSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
404: {"description": "Patch name 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(PatchNameSchema)
async def get(self) -> Response:
"""
get package patch
Returns:
Response: 200 with package patch on success
Raises:
HTTPNotFound: if package base is unknown
"""
package_base = self.request.match_info["package"]
variable = self.request.match_info["patch"]
patches = self.service().patches_get(package_base, variable)
selected = next((patch for patch in patches if patch.key == variable), None)
if selected is None:
raise HTTPNotFound
return json_response(selected.view())

View File

@ -0,0 +1,108 @@
#
# 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 HTTPBadRequest, HTTPNoContent, Response, json_response
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PatchSchema
from ahriman.web.views.base import BaseView
class PatchesView(BaseView):
"""
package patches web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/patches"]
@aiohttp_apispec.docs(
tags=["Packages"],
summary="Get package patches",
description="Retrieve all package patches",
responses={
200: {"description": "Success response", "schema": PatchSchema(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)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
async def get(self) -> Response:
"""
get package patches
Returns:
Response: 200 with package patches on success
Raises:
HTTPNotFound: if package base is unknown
"""
package_base = self.request.match_info["package"]
patches = self.service().patches_get(package_base, None)
response = [patch.view() for patch in patches]
return json_response(response)
@aiohttp_apispec.docs(
tags=["Packages"],
summary="Update package patch",
description="Update or create package patch",
responses={
204: {"description": "Success response"},
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
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)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.json_schema(PatchSchema)
async def post(self) -> None:
"""
update or create package patch
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
data = await self.extract_data()
try:
key = data["key"]
value = data["value"]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service().patches_update(package_base, PkgbuildPatch(key, value))
raise HTTPNoContent

View File

@ -19,11 +19,10 @@
#
import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPNotFound, Response, json_response
from aiohttp.web import Response, json_response
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchemaV2, PackageNameSchema, PaginationSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, PackageNameSchema, PaginationSchema
from ahriman.web.views.base import BaseView
@ -43,7 +42,7 @@ class LogsView(BaseView):
summary="Get paginated package logs",
description="Retrieve package logs and the last package status",
responses={
200: {"description": "Success response", "schema": LogsSchemaV2},
200: {"description": "Success response", "schema": LogSchema(many=True)},
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
@ -67,16 +66,12 @@ class LogsView(BaseView):
"""
package_base = self.request.match_info["package"]
limit, offset = self.page()
try:
_, status = self.service().package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
logs = self.service().logs_get(package_base, limit, offset)
response = {
"package_base": package_base,
"status": status.view(),
"logs": logs,
}
response = [
{
"created": created,
"message": message,
} for created, message in logs
]
return json_response(response)