Compare commits

..

17 Commits

945 changed files with 4888 additions and 16173 deletions
-3
View File
@@ -12,6 +12,3 @@ __pycache__/
*.pyc
*.pyd
*.pyo
node_modules/
package-lock.json
-6
View File
@@ -26,10 +26,6 @@ jobs:
- uses: docker/setup-buildx-action@v3
- name: Set image date
id: args
run: echo "::set-output name=date::$(date -d yesterday +'%Y-%m-%d')"
- name: Login to docker hub
uses: docker/login-action@v3
with:
@@ -57,8 +53,6 @@ jobs:
- name: Build an image and push
uses: docker/build-push-action@v6
with:
build-args: |
BUILD_DATE=${{ steps.args.outputs.date }}
file: docker/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
+3 -3
View File
@@ -10,13 +10,13 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
# refresh the image
pacman -Syyu --noconfirm
# main dependencies
pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-inflection python-pyelftools python-requests python-systemd sudo
# make dependencies
pacman -S --noconfirm --asdeps base-devel python-build python-hatchling python-installer python-tox python-wheel
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
# optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then
# web server
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-aiohttp-sse-git python-cryptography python-jinja
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja
# additional features
pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync
fi
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
- ${{ github.workspace }}:/build
steps:
- run: pacman --noconfirm -Syu base-devel git npm python-tox
- run: pacman --noconfirm -Syu base-devel git python-tox
- run: git config --global --add safe.directory *
-9
View File
@@ -99,12 +99,3 @@ status_cache.json
*.db
docs/html/
# Frontend
node_modules/
package-lock.json
ahriman-web/package/share/ahriman/templates/static/index.js
ahriman-web/package/share/ahriman/templates/static/index.css
# local configs
/*.ini
-2
View File
@@ -2,6 +2,4 @@
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
asyncio_default_fixture_loop_scope = function
asyncio_mode = auto
pythonpath = tests
resource-path.directory-name-test-resources = ../../tests/testresources
spec_test_format = {result} {docstring_summary}
+2 -2
View File
@@ -1,9 +1,9 @@
version: 2
build:
os: ubuntu-lts-latest
os: ubuntu-20.04
tools:
python: "3.13"
python: "3.12"
apt_packages:
- graphviz
+3 -7
View File
@@ -125,7 +125,7 @@ Again, the most checks can be performed by `tox` command, though some additional
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
```
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Same idea applies to frontend classes.
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
@@ -172,9 +172,8 @@ Again, the most checks can be performed by `tox` command, though some additional
)
```
* Imports goes in alphabetical order, no relative imports allowed. Same rule applies to frontend classes.
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. Same rule applies to frontend classes.
* It is possible to create file which contains some functions (e.g. `ahriman.core.utils`), but in this case you would need to define `__all__` attribute.
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute.
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
@@ -221,14 +220,11 @@ Again, the most checks can be performed by `tox` command, though some additional
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
* `Generator` return type is not allowed. Generator functions must return generic `Iterator` object. Documentation should be described as `Yields`, however, because of pylint checks. Unfortunately, `Iterable` return type is not available for generators also, because of specific `contextlib.contextmanager` case.
### Other checks
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
Frontend checks normally are performed by `eslint` (e.g. `npx run eslint`).
## Developers how to
### Run automated checks
-68
View File
@@ -1,68 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ahriman-core"
description = "ArcH linux ReposItory MANager, core package"
readme = "../README.md"
requires-python = ">=3.13"
license = {file = "../COPYING"}
authors = [
{name = "ahriman team"},
]
dependencies = [
"bcrypt",
"filelock",
"inflection",
"pyelftools",
"requests",
]
dynamic = ["version"]
[project.optional-dependencies]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
reports = [
"Jinja2",
]
s3 = [
"boto3",
]
shell = [
"IPython",
]
stats = [
"matplotlib",
]
unixsocket = [
"requests-unixsocket2",
]
validator = [
"cerberus",
]
[project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.urls]
Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases"
[tool.hatch.version]
path = "src/ahriman/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/ahriman"]
[tool.hatch.build.targets.wheel.shared-data]
"package/lib" = "lib"
"package/share" = "share"
@@ -1,81 +0,0 @@
#
# Copyright (c) 2021-2026 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 argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PackagePrinter
from ahriman.models.action import Action
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.repository_id import RepositoryId
class Archives(Handler):
"""
package archives handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=True)
match args.action:
case Action.List:
archives = application.repository.package_archives(args.package)
for package in archives:
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
Archives.check_status(args.exit_code, bool(archives))
@staticmethod
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package archives subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-archives", help="list package archive versions",
description="list available archive versions for the package")
parser.add_argument("package", help="package base")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.add_argument("--info", help="show additional package information",
action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_package_archives_parser]
@@ -1,93 +0,0 @@
#
# Copyright (c) 2021-2026 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 argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.models.action import Action
from ahriman.models.repository_id import RepositoryId
class Hold(Handler):
"""
package hold handler
"""
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
client = Application(repository_id, configuration, report=True).reporter
match args.action:
case Action.Remove:
for package in args.package:
client.package_hold_update(package, enabled=False)
case Action.Update:
for package in args.package:
client.package_hold_update(package, enabled=True)
@staticmethod
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for hold package subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-hold", help="hold package",
description="hold package from automatic updates")
parser.add_argument("package", help="package base", nargs="+")
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for unhold package subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-unhold", help="unhold package",
description="remove package hold, allowing automatic updates")
parser.add_argument("package", help="package base", nargs="+")
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [
_set_package_hold_parser,
_set_package_unhold_parser,
]
@@ -1,100 +0,0 @@
#
# Copyright (c) 2021-2026 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 argparse
from dataclasses import replace
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PkgbuildPrinter
from ahriman.models.action import Action
from ahriman.models.repository_id import RepositoryId
class Pkgbuild(Handler):
"""
package pkgbuild handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
client = Application(repository_id, configuration, report=True).reporter
match args.action:
case Action.List:
changes = client.package_changes_get(args.package)
PkgbuildPrinter(changes)(verbose=True, separator="")
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
case Action.Remove:
changes = client.package_changes_get(args.package)
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
@staticmethod
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package pkgbuild subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
description="retrieve package PKGBUILD stored in database",
epilog="This command requests package status from the web interface "
"if it is available.")
parser.add_argument("package", help="package base")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
action="store_true")
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
return parser
@staticmethod
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package pkgbuild remove subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
description="remove the package PKGBUILD stored remotely")
parser.add_argument("package", help="package base")
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
return parser
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
@@ -1,131 +0,0 @@
#
# Copyright (c) 2021-2026 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 argparse
from dataclasses import replace
from pathlib import Path
from ahriman.application.application import Application
from ahriman.application.handlers.add import Add
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.utils import extract_user
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.repository_id import RepositoryId
class Rollback(Handler):
"""
package rollback handler
"""
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
application.on_start()
package = Rollback.package_load(application, args.package, args.version)
artifacts = Rollback.package_artifacts(application, package)
args.package = [str(artifact) for artifact in artifacts]
Add.perform_action(application, args)
if args.hold:
application.reporter.package_hold_update(package.base, enabled=True)
@staticmethod
def _set_package_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package rollback subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-rollback", help="rollback package",
description="rollback package to specified version from archives")
parser.add_argument("package", help="package base")
parser.add_argument("version", help="package version")
parser.add_argument("--hold", help="hold package afterwards",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
exit_code=True, increment=False, now=True, local=False, manual=False, refresh=False,
source=PackageSource.Archive, variable=None, vcs=False)
return parser
@staticmethod
def package_artifacts(application: Application, package: Package) -> list[Path]:
"""
look for requested package artifacts and return paths to them
Args:
application(Application): application instance
package(Package): package descriptor
Returns:
list[Path]: paths to found artifacts
Raises:
UnknownPackageError: if artifacts do not exist
"""
# lookup for built artifacts
artifacts = application.repository.package_archives_lookup(package)
if not artifacts:
raise UnknownPackageError(package.base)
return artifacts
@staticmethod
def package_load(application: Application, package_base: str, version: str) -> Package:
"""
load package from repository, while setting requested version
Args:
application(Application): application instance
package_base(str): package base
version(str): package version
Returns:
Package: loaded package
Raises:
UnknownPackageError: if package does not exist
"""
try:
package, _ = next(iter(application.reporter.package_get(package_base)))
return replace(package, version=version)
except StopIteration:
raise UnknownPackageError(package_base) from None
arguments = [_set_package_rollback_parser]
@@ -1,81 +0,0 @@
#
# Copyright (c) 2021-2026 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 pathlib import Path
from pyalpm import Handle, Package # type: ignore[import-not-found]
from tempfile import TemporaryDirectory
from typing import Any, ClassVar, Self
class PacmanHandle:
"""
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
Attributes:
handle(Handle): pyalpm handle instance
"""
_ephemeral: ClassVar[Self | None] = None
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Args:
*args(Any): positional arguments for :class:`pyalpm.Handle`
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
"""
self.handle = Handle(*args, **kwargs)
@classmethod
def ephemeral(cls) -> Self:
"""
create temporary instance with no access to real databases
Returns:
Self: loaded class
"""
if cls._ephemeral is None:
# handle creates alpm version file, but we don't use it
# so it is ok to just remove it
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
cls._ephemeral = cls("/", dir_name)
return cls._ephemeral
def package_load(self, path: Path) -> Package:
"""
load package from path to the archive
Args:
path(Path): path to package archive
Returns:
Package: package instance
"""
return self.handle.load_pkg(str(path))
def __getattr__(self, item: str) -> Any:
"""
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
Args:
item(str): property name
Returns:
Any: attribute by its name
"""
return self.handle.__getattribute__(item)
@@ -1,114 +0,0 @@
#
# Copyright (c) 2021-2026 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.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.utils import full_version, utcnow
from ahriman.models.package import Package
from ahriman.models.pkgbuild import Pkgbuild
class PackageVersion(LazyLogging):
"""
package version extractor and helper
Attributes:
package(Package): package definitions
"""
def __init__(self, package: Package) -> None:
"""
Args:
package(Package): package definitions
"""
self.package = package
def actual_version(self, configuration: Configuration) -> str:
"""
additional method to handle VCS package versions
Args:
configuration(Configuration): configuration instance
Returns:
str: package version if package is not VCS and current version according to VCS otherwise
"""
if not self.package.is_vcs:
return self.package.version
_, repository_id = configuration.check_loaded()
paths = configuration.repository_paths
task = Task(self.package, configuration, repository_id.architecture, paths)
try:
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
task.init(paths.cache_for(self.package.base), [], None)
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.package.base) / "PKGBUILD")
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
except Exception:
self.logger.exception("cannot determine version of VCS package")
finally:
# clear log files generated by devtools
for log_file in paths.cache_for(self.package.base).glob("*.log"):
log_file.unlink()
return self.package.version
def is_newer_than(self, timestamp: float | int) -> bool:
"""
check if package was built after the specified timestamp
Args:
timestamp(float | int): timestamp to check build date against
Returns:
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
In case if build date is not set by any of packages, it returns False
"""
return any(
package.build_date > timestamp
for package in self.package.packages.values()
if package.build_date is not None
)
def is_outdated(self, remote: Package, configuration: Configuration, *,
calculate_version: bool = True) -> bool:
"""
check if package is out-of-dated
Args:
remote(Package): package properties from remote source
configuration(Configuration): configuration instance
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
(Default value = True)
Returns:
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
"""
vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
if calculate_version and not self.is_newer_than(min_vcs_build_date):
remote_version = PackageVersion(remote).actual_version(configuration)
else:
remote_version = remote.version
return self.package.vercmp(remote_version) < 0
@@ -1,25 +0,0 @@
#
# Copyright (c) 2021-2026 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/>.
#
__all__ = ["steps"]
steps = [
"""alter table package_changes add column pkgbuild text""",
]
@@ -1,25 +0,0 @@
#
# Copyright (c) 2021-2026 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/>.
#
__all__ = ["steps"]
steps = [
"""alter table package_statuses add column is_held integer not null default 0""",
]
@@ -1,62 +0,0 @@
#
# Copyright (c) 2021-2026 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.core.formatters.printer import Printer
from ahriman.models.changes import Changes
from ahriman.models.property import Property
class PkgbuildPrinter(Printer):
"""
print content of the pkgbuild stored in changes
Attributes:
changes(Changes): package changes
"""
def __init__(self, changes: Changes) -> None:
"""
Args:
changes(Changes): package changes
"""
Printer.__init__(self)
self.changes = changes
def properties(self) -> list[Property]:
"""
convert content into printable data
Returns:
list[Property]: list of content properties
"""
if self.changes.pkgbuild is None:
return []
return [Property("", self.changes.pkgbuild, is_required=True, indent=0)]
# pylint: disable=redundant-returns-doc
def title(self) -> str | None:
"""
generate entry title from content
Returns:
str | None: content title if it can be generated and ``None`` otherwise
"""
if self.changes.pkgbuild is None:
return None
return self.changes.last_commit_sha
@@ -1,108 +0,0 @@
#
# Copyright (c) 2021-2026 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 contextvars
import logging
from typing import Any, ClassVar, TypeVar, cast
T = TypeVar("T")
class LogContext:
"""
logging context manager which provides context variables injection into log records
"""
_context: ClassVar[dict[str, contextvars.ContextVar[Any]]] = {}
@classmethod
def get(cls, name: str) -> T | None:
"""
get context variable if available
Args:
name(str): name of the context variable
Returns:
T | None: context variable if available and ``None`` otherwise
"""
if (variable := cls._context.get(name)) is not None:
return cast(T | None, variable.get())
return None
@classmethod
def log_record_factory(cls, *args: Any, **kwargs: Any) -> logging.LogRecord:
"""
log record factory which injects all registered context variables into log records
Args:
*args(Any): positional arguments for the log factory
**kwargs(Any): keyword arguments for the log factory
Returns:
logging.LogRecord: log record with context variables set as attributes
"""
record = logging.LogRecord(*args, **kwargs)
for name, variable in cls._context.items():
if (value := variable.get()) is not None:
setattr(record, name, value)
return record
@classmethod
def register(cls, name: str) -> contextvars.ContextVar[T]:
"""
(re)create context variable for log records
Args:
name(str): name of the context variable
Returns:
contextvars.ContextVar[T]: created context variable
"""
variable = cls._context[name] = contextvars.ContextVar(name, default=None)
return variable
@classmethod
def reset(cls, name: str, token: contextvars.Token[T]) -> None:
"""
reset context variable to its previous value
Args:
name(str): attribute name to reset on log records
token(contextvars.Token[T]): previously registered token
"""
cls._context[name].reset(token)
@classmethod
def set(cls, name: str, value: T) -> contextvars.Token[T]:
"""
set context variable for log records. This value will be automatically emitted with each log record
Args:
name(str): attribute name to set on log records
value(T): current value of the context variable
Returns:
contextvars.Token[T]: token created with this value
"""
return cls._context[name].set(value)
@@ -1,70 +0,0 @@
#
# Copyright (c) 2021-2026 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 collections.abc import Iterable
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
class Explorer:
"""
helper to read filesystem and find created repositories
"""
@staticmethod
def repositories_extract(configuration: Configuration, repository: str | None = None,
architecture: str | None = None) -> list[RepositoryId]:
"""
get known architectures
Args:
configuration(Configuration): configuration instance
repository(str | None, optional): predefined repository name if available (Default value = None)
architecture(str | None, optional): predefined repository architecture if available (Default value = None)
Returns:
list[RepositoryId]: list of repository names and architectures for which tree is created
"""
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# extract repository names first
if repository is not None:
repositories: Iterable[str] = [repository]
elif from_filesystem := RepositoryPaths.known_repositories(root):
repositories = from_filesystem
else: # try to read configuration now
repositories = [configuration.get("repository", "name")]
# extract architecture names
if architecture is not None:
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
)
else: # try to read from file system
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
for architecture in RepositoryPaths.known_architectures(root, repository)
)
return sorted(parsed)
@@ -1,239 +0,0 @@
#
# Copyright (c) 2021-2026 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 copy
from collections.abc import Callable, Iterable
from functools import cmp_to_key
from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.build_tools.sources import Sources
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.status import Client
from ahriman.core.utils import list_flatmap, package_like
from ahriman.models.changes import Changes
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
class PackageInfo(LazyLogging):
"""
handler for the package information
Attributes:
configuration(Configuration): configuration instance
pacman(Pacman): alpm wrapper instance
paths(RepositoryPaths): repository paths instance
reporter(Client): build status reporter instance
repository_id(RepositoryId): repository unique identifier
"""
configuration: Configuration
pacman: Pacman
paths: RepositoryPaths
reporter: Client
repository_id: RepositoryId
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
"""
generate full dependencies list including transitive dependencies
Args:
package(Package): package to check dependencies for
packages(Iterable[Package]): repository package list
Returns:
list[str]: all dependencies of the package
"""
dependencies = {}
# load own package dependencies
for package_base in packages:
for name, repo_package in package_base.packages.items():
dependencies[name] = repo_package.depends
for provides in repo_package.provides:
dependencies[provides] = repo_package.depends
# load repository dependencies
for database in self.pacman.handle.get_syncdbs():
for pacman_package in database.pkgcache:
dependencies[pacman_package.name] = pacman_package.depends
for provides in pacman_package.provides:
dependencies[provides] = pacman_package.depends
result = set(package.depends)
current_depends: set[str] = set()
while result != current_depends:
current_depends = copy.deepcopy(result)
for package_name in current_depends:
result.update(dependencies.get(package_name, []))
return sorted(result)
def load_archives(self, packages: Iterable[Path], *, latest_only: bool = True) -> list[Package]:
"""
load packages from list of archives
Args:
packages(Iterable[Path]): paths to package archives
latest_only(bool, optional): filter packages with the same base, keeping only fresh packages installed
(Default value = True)
Returns:
list[Package]: list of read packages
"""
sources = {package.base: package.remote for package, _, in self.reporter.package_get(None)}
result: dict[str, dict[str, Package]] = {}
# we are iterating over bases, not single packages
for full_path in packages:
try:
local = Package.from_archive(full_path)
if (source := sources.get(local.base)) is not None: # update source with remote
local.remote = source
loaded_versions = result.setdefault(local.base, {})
current = loaded_versions.setdefault(local.version, local)
current.packages.update(local.packages)
except Exception:
self.logger.exception("could not load package from %s", full_path)
if latest_only:
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
for package_base, versions in result.items():
newest = max(versions.values(), key=cmp_to_key(comparator))
result[package_base] = {newest.version: newest}
return [
package
for versions in result.values()
for package in versions.values()
]
def package_archives(self, package_base: str) -> list[Package]:
"""
load list of packages known for this package base. This method unlike
:func:`ahriman.core.repository.package_info.PackageInfo.load_archives` scans archive directory and loads all
versions available for the ``package_base``
Args:
package_base(str): package base
Returns:
list[Package]: list of packages belonging to this base, sorted by version by ascension
"""
archive = self.paths.archive_for(package_base)
if not archive.is_dir():
return []
packages = self.load_archives(filter(package_like, archive.iterdir()), latest_only=False)
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
return sorted(
(package for package in packages if package.supports_architecture(self.repository_id.architecture)),
key=cmp_to_key(comparator),
)
def package_archives_lookup(self, package: Package) -> list[Path]:
"""
check if there is a rebuilt package already
Args:
package(Package): package to check
Returns:
list[Path]: list of built packages and signatures if available, empty list otherwise
"""
archive = self.paths.archive_for(package.base)
for built in self.package_archives(package.base):
if built.version != package.version:
continue
return list_flatmap(built.packages.values(), lambda single: archive.glob(f"{single.filename}*"))
return []
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
"""
extract package change for the package since last commit if available
Args:
package(Package): package properties
last_commit_sha(str): last known commit hash
Returns:
Changes | None: changes if available
"""
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
dir_path = Path(dir_name)
patches = self.reporter.package_patches_get(package.base, None)
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
if current_commit_sha != last_commit_sha:
return Sources.changes(dir_path, last_commit_sha)
return None
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
"""
generate list of repository packages
Args:
filter_packages(Iterable[str] | None, optional): filter packages list by specified only
Returns:
list[Package]: list of packages properties
"""
packages = self.load_archives(filter(package_like, self.paths.repository.iterdir()))
if filter_packages:
packages = [package for package in packages if package.base in filter_packages]
return packages
def packages_built(self) -> list[Path]:
"""
get list of files in built packages directory
Returns:
list[Path]: list of filenames from the directory
"""
return list(filter(package_like, self.paths.packages.iterdir()))
def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]:
"""
extract list of packages which depends on specified package
Args:
packages(list[Package]): list of packages to be filtered
depends_on(Iterable[str] | None): dependencies of the packages
Returns:
list[Package]: list of repository packages which depend on specified packages
"""
if depends_on is None:
return packages # no list provided extract everything by default
depends_on = set(depends_on)
return [
package
for package in packages
if depends_on.intersection(self.full_depends(package, packages))
]
@@ -1,136 +0,0 @@
#
# Copyright (c) 2021-2026 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 uuid
from asyncio import Lock, Queue, QueueFull, QueueShutDown
from dataclasses import dataclass
from typing import Any
from ahriman.core.log import LazyLogging
from ahriman.models.event import EventType
SSEvent = tuple[str, dict[str, Any]]
@dataclass(frozen=True)
class _Subscription:
"""
internal event bus subscription record
Attributes:
topics(list[EventType] | None): event type filter, ``None`` means all
object_id(str | None): object identifier filter, ``None`` means all
queue(Queue[SSEvent]): per-subscriber event queue
"""
topics: list[EventType] | None
object_id: str | None
queue: Queue[SSEvent]
class EventBus(LazyLogging):
"""
event bus implementation
Attributes:
max_size(int): maximum size of queue
"""
def __init__(self, max_size: int) -> None:
"""
Args:
max_size(int): maximum size of queue
"""
self.max_size = max_size
self._lock = Lock()
self._subscribers: dict[str, _Subscription] = {}
async def broadcast(self, event_type: EventType, object_id: str | None, **kwargs: Any) -> None:
"""
broadcast event to all subscribers
Args:
event_type(EventType): event type
object_id(str | None): object identifier (e.g. package base)
**kwargs(Any): additional event data
"""
event: dict[str, Any] = {"object_id": object_id}
event.update(kwargs)
async with self._lock:
snapshot = list(self._subscribers.items())
for subscriber_id, subscription in snapshot:
if subscription.topics is not None and event_type not in subscription.topics:
continue
if subscription.object_id is not None and object_id != subscription.object_id:
continue
try:
subscription.queue.put_nowait((event_type, event))
except QueueFull:
self.logger.warning("discard message to slow subscriber %s", subscriber_id)
except QueueShutDown:
pass
async def shutdown(self) -> None:
"""
gracefully shutdown all subscribers
"""
async with self._lock:
for subscription in self._subscribers.values():
subscription.queue.shutdown()
self._subscribers.clear()
async def subscribe(self, topics: list[EventType] | None = None,
object_id: str | None = None) -> tuple[str, Queue[SSEvent]]:
"""
register new subscriber
Args:
topics(list[EventType] | None, optional): list of event types to filter by. If ``None`` is set,
all events will be delivered (Default value = None)
object_id(str | None, optional): object identifier to filter by. If ``None`` is set,
events for all objects will be delivered (Default value = None)
Returns:
tuple[str, Queue[SSEvent]]: subscriber identifier and associated queue
"""
subscriber_id = str(uuid.uuid4())
queue: Queue[SSEvent] = Queue(self.max_size)
async with self._lock:
self._subscribers[subscriber_id] = _Subscription(topics=topics, object_id=object_id, queue=queue)
return subscriber_id, queue
async def unsubscribe(self, subscriber_id: str) -> None:
"""
unsubscribe from events
Args:
subscriber_id(str): subscriber unique identifier
"""
async with self._lock:
subscription = self._subscribers.pop(subscriber_id, None)
if subscription is not None:
subscription.queue.shutdown()
@@ -1,361 +0,0 @@
#
# Copyright (c) 2021-2026 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/>.
#
# pylint: disable=too-many-public-methods
from asyncio import Lock
from dataclasses import replace
from typing import Self
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.log import LazyLogging
from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.status import Client
from ahriman.core.status.event_bus import EventBus
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.event import Event, EventType
from ahriman.models.log_record import LogRecord
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class Watcher(LazyLogging):
"""
package status watcher
Attributes:
client(Client): reporter instance
event_bus(EventBus): event bus instance
package_info(PackageInfo): package info instance
status(BuildStatus): daemon status
"""
def __init__(self, client: Client, package_info: PackageInfo, event_bus: EventBus) -> None:
"""
Args:
client(Client): reporter instance
package_info(PackageInfo): package info instance
event_bus(EventBus): event bus instance
"""
self.client = client
self.package_info = package_info
self.event_bus = event_bus
self._lock = Lock()
self._known: dict[str, tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
async def event_add(self, event: Event) -> None:
"""
create new event
Args:
event(Event): audit log event
"""
self.client.event_add(event)
async def event_get(self, event: str | EventType | None, object_id: str | None,
from_date: int | float | None = None, to_date: int | float | None = None,
limit: int = -1, offset: int = 0) -> list[Event]:
"""
retrieve list of events
Args:
event(str | EventType | None): filter by event type
object_id(str | None): filter by event object
from_date(int | float | None, optional): minimal creation date, inclusive (Default value = None)
to_date(int | float | None, optional): maximal creation date, exclusive (Default value = None)
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
Returns:
list[Event]: list of audit log events
"""
return self.client.event_get(event, object_id, from_date, to_date, limit, offset)
async def load(self) -> None:
"""
load packages from local database
"""
async with self._lock:
self._known = {
package.base: (package, status)
for package, status in self.client.package_get(None)
}
async def logs_rotate(self, keep_last_records: int) -> None:
"""
remove older logs from storage
Args:
keep_last_records(int): number of last records to keep
"""
self.client.logs_rotate(keep_last_records)
async def package_archives(self, package_base: str) -> list[Package]:
"""
get known package archives
Args:
package_base(str): package base
Returns:
list[Package]: list of built package for this package base
"""
return self.package_info.package_archives(package_base)
async def package_changes_get(self, package_base: str) -> Changes:
"""
get package changes
Args:
package_base(str): package base to retrieve
Returns:
Changes: package changes if available and empty object otherwise
"""
return self.client.package_changes_get(package_base)
async def package_changes_update(self, package_base: str, changes: Changes) -> None:
"""
update package changes
Args:
package_base(str): package base to update
changes(Changes): changes descriptor
"""
self.client.package_changes_update(package_base, changes)
async def package_dependencies_get(self, package_base: str) -> Dependencies:
"""
get package dependencies
Args:
package_base(str): package base to retrieve
Returns:
list[Dependencies]: package implicit dependencies if available
"""
return self.client.package_dependencies_get(package_base)
async def package_dependencies_update(self, package_base: str, dependencies: Dependencies) -> None:
"""
update package dependencies
Args:
package_base(str): package base to update
dependencies(Dependencies): dependencies descriptor
"""
self.client.package_dependencies_update(package_base, dependencies)
async def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
"""
get current package base build status
Args:
package_base(str): package base
Returns:
tuple[Package, BuildStatus]: package and its status
Raises:
UnknownPackageError: if no package found
"""
try:
async with self._lock:
return self._known[package_base]
except KeyError:
raise UnknownPackageError(package_base) from None
async def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
"""
update package hold status
Args:
package_base(str): package base name
enabled(bool): new hold status
"""
package, status = await self.package_get(package_base)
async with self._lock:
self._known[package_base] = (package, replace(status, is_held=enabled))
self.client.package_hold_update(package_base, enabled=enabled)
await self.event_bus.broadcast(EventType.PackageHeld, package_base, is_held=enabled)
async def package_logs_add(self, log_record: LogRecord) -> None:
"""
post log record
Args:
log_record(LogRecord): log record
"""
self.client.package_logs_add(log_record)
await self.event_bus.broadcast(EventType.BuildLog, log_record.log_record_id.package_base, **log_record.view())
async def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
limit: int = -1, offset: int = 0) -> list[LogRecord]:
"""
get package logs
Args:
package_base(str): package base
version(str | None, optional): package version to search (Default value = None)
process_id(str | None, optional): process identifier to search (Default value = None)
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
Returns:
list[LogRecord]: package logs
"""
return self.client.package_logs_get(package_base, version, process_id, limit, offset)
async def package_logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package logs
Args:
package_base(str): package base
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
"""
self.client.package_logs_remove(package_base, version)
async def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
"""
get package patches
Args:
package_base(str): package base to retrieve
variable(str | None): optional filter by patch variable
Returns:
list[PkgbuildPatch]: list of patches for the specified package
"""
return self.client.package_patches_get(package_base, variable)
async def package_patches_remove(self, package_base: str, variable: str | None) -> None:
"""
remove package patch
Args:
package_base(str): package base to update
variable(str | None): patch name. If ``None`` is set, all patches will be removed
"""
self.client.package_patches_remove(package_base, variable)
async def package_patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
"""
create or update package patch
Args:
package_base(str): package base to update
patch(PkgbuildPatch): package patch
"""
self.client.package_patches_update(package_base, patch)
async def package_remove(self, package_base: str) -> None:
"""
remove package base from known list if any
Args:
package_base(str): package base
"""
async with self._lock:
self._known.pop(package_base, None)
self.client.package_remove(package_base)
await self.event_bus.broadcast(EventType.PackageRemoved, package_base)
async def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package status
Args:
package_base(str): package base to update
status(BuildStatusEnum): new build status
"""
package, current_status = await self.package_get(package_base)
async with self._lock:
self._known[package_base] = (package, BuildStatus(status, is_held=current_status.is_held))
self.client.package_status_update(package_base, status)
await self.event_bus.broadcast(EventType.PackageStatusChanged, package_base, status=status.value)
async def package_update(self, package: Package, status: BuildStatusEnum) -> None:
"""
update package
Args:
package(Package): package description
status(BuildStatusEnum): new build status
"""
async with self._lock:
_, current_status = self._known.get(package.base, (package, BuildStatus()))
self._known[package.base] = (package, BuildStatus(status, is_held=current_status.is_held))
self.client.package_update(package, status)
await self.event_bus.broadcast(
EventType.PackageUpdated, package.base, status=status.value, version=package.version,
)
async def packages(self) -> list[tuple[Package, BuildStatus]]:
"""
get current known packages list
Returns:
list[tuple[Package, BuildStatus]]: list of packages together with their statuses
"""
async with self._lock:
return list(self._known.values())
async def shutdown(self) -> None:
"""
gracefully shutdown watcher
"""
await self.event_bus.shutdown()
async def status_update(self, status: BuildStatusEnum) -> None:
"""
update service status
Args:
status(BuildStatusEnum): new service status
"""
self.status = BuildStatus(status)
await self.event_bus.broadcast(EventType.ServiceStatusChanged, None, status=status.value)
def __call__(self, package_base: str | None) -> Self:
"""
extract client for future calls
Args:
package_base(str | None): package base to validate that package exists if applicable
Returns:
Self: instance of self to pass calls to the client
Raises:
UnknownPackageError: if no package found
"""
# keep check here instead of calling package_get to keep this method synchronized
if package_base is not None and package_base not in self._known:
raise UnknownPackageError(package_base)
return self
@@ -1,85 +0,0 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers.add import Add
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.package_source import PackageSource
from ahriman.models.pkgbuild_patch import PkgbuildPatch
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.package = ["ahriman"]
args.now = False
args.refresh = 0
args.source = PackageSource.Auto
args.username = "username"
args.variable = None
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
perform_mock = mocker.patch("ahriman.application.handlers.add.Add.perform_action")
_, repository_id = configuration.check_loaded()
Add.run(args, repository_id, configuration, report=False)
on_start_mock.assert_called_once_with()
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
def test_perform_action(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must perform add action
"""
args = _default_args(args)
application_mock = mocker.patch("ahriman.application.application.Application.add")
update_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
Add.perform_action(application, args)
application_mock.assert_called_once_with(args.package, args.source, args.username)
update_mock.assert_not_called()
def test_perform_action_with_patches(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must perform add action and insert temporary patches
"""
args = _default_args(args)
args.variable = ["KEY=VALUE"]
mocker.patch("ahriman.application.application.Application.add")
patches_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_update")
Add.perform_action(application, args)
patches_mock.assert_called_once_with(args.package[0], PkgbuildPatch("KEY", "VALUE"))
def test_perform_action_with_updates(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
"""
must perform add action with updates after
"""
args = _default_args(args)
args.now = True
mocker.patch("ahriman.application.application.Application.add")
update_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
Add.perform_action(application, args)
update_mock.assert_called_once_with(application, args)
@@ -1,84 +0,0 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.handlers.archives import Archives
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.repository import Repository
from ahriman.models.action import Action
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.action = Action.List
args.exit_code = False
args.info = False
args.package = "package"
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
return_value=[package_ahriman])
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
_, repository_id = configuration.check_loaded()
Archives.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(args.package)
check_mock.assert_called_once_with(False, True)
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty archives result
"""
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
_, repository_id = configuration.check_loaded()
Archives.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
mocker: MockerFixture) -> None:
"""
must create application object with native reporting
"""
args = _default_args(args)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
load_mock = mocker.patch("ahriman.core.repository.Repository.load")
_, repository_id = configuration.check_loaded()
Archives.run(args, repository_id, configuration, report=False)
load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Archives.ALLOW_MULTI_ARCHITECTURE_RUN
@@ -1,53 +0,0 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.handlers.hold import Hold
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.action import Action
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.package = ["ahriman"]
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
args.action = Action.Update
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
_, repository_id = configuration.check_loaded()
Hold.run(args, repository_id, configuration, report=False)
hold_mock.assert_called_once_with("ahriman", enabled=True)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must remove held status
"""
args = _default_args(args)
args.action = Action.Remove
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
_, repository_id = configuration.check_loaded()
Hold.run(args, repository_id, configuration, report=False)
hold_mock.assert_called_once_with("ahriman", enabled=False)
@@ -1,100 +0,0 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.handlers.pkgbuild import Pkgbuild
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.repository import Repository
from ahriman.models.action import Action
from ahriman.models.changes import Changes
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.action = Action.List
args.exit_code = False
args.package = "package"
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
return_value=Changes("sha", "change", "pkgbuild content"))
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
_, repository_id = configuration.check_loaded()
Pkgbuild.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(args.package)
check_mock.assert_called_once_with(False, True)
print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator="")
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty pkgbuild result
"""
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=Changes())
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
_, repository_id = configuration.check_loaded()
Pkgbuild.run(args, repository_id, configuration, report=False)
check_mock.assert_called_once_with(True, False)
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must remove package pkgbuild
"""
args = _default_args(args)
args.action = Action.Remove
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
changes = Changes("sha", "change", "pkgbuild content")
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
_, repository_id = configuration.check_loaded()
Pkgbuild.run(args, repository_id, configuration, report=False)
update_mock.assert_called_once_with(args.package, Changes("sha", "change", None))
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
mocker: MockerFixture) -> None:
"""
must create application object with native reporting
"""
args = _default_args(args)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
load_mock = mocker.patch("ahriman.core.repository.Repository.load")
_, repository_id = configuration.check_loaded()
Pkgbuild.run(args, repository_id, configuration, report=False)
load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Pkgbuild.ALLOW_MULTI_ARCHITECTURE_RUN
@@ -1,113 +0,0 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers.rollback import Rollback
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository import Repository
from ahriman.models.package import Package
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.package = "ahriman"
args.version = "1.0.0-1"
args.hold = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
artifacts = [package.filepath for package in package_ahriman.packages.values()]
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
load_mock = mocker.patch("ahriman.application.handlers.rollback.Rollback.package_load",
return_value=package_ahriman)
artifacts_mock = mocker.patch("ahriman.application.handlers.rollback.Rollback.package_artifacts",
return_value=artifacts)
perform_mock = mocker.patch("ahriman.application.handlers.add.Add.perform_action")
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
_, repository_id = configuration.check_loaded()
Rollback.run(args, repository_id, configuration, report=False)
on_start_mock.assert_called_once_with()
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, args.version)
artifacts_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
hold_mock.assert_not_called()
def test_run_hold(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must hold package after rollback
"""
args = _default_args(args)
args.hold = True
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.application.Application.on_start")
mocker.patch("ahriman.application.handlers.rollback.Rollback.package_load", return_value=package_ahriman)
mocker.patch("ahriman.application.handlers.rollback.Rollback.package_artifacts", return_value=[])
mocker.patch("ahriman.application.handlers.add.Add.perform_action")
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
_, repository_id = configuration.check_loaded()
Rollback.run(args, repository_id, configuration, report=False)
hold_mock.assert_called_once_with(package_ahriman.base, enabled=True)
def test_package_artifacts(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package artifacts
"""
artifacts = [package.filepath for package in package_ahriman.packages.values()]
lookup_mock = mocker.patch("ahriman.core.repository.Repository.package_archives_lookup", return_value=artifacts)
assert Rollback.package_artifacts(application, package_ahriman) == artifacts
lookup_mock.assert_called_once_with(package_ahriman)
def test_package_artifacts_empty(application: Application, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must raise UnknownPackageError if no artifacts found
"""
mocker.patch("ahriman.core.repository.Repository.package_archives_lookup", return_value=[])
with pytest.raises(UnknownPackageError):
Rollback.package_artifacts(application, package_ahriman)
def test_package_load(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package from reporter
"""
package_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
return_value=[(package_ahriman, None)])
result = Rollback.package_load(application, package_ahriman.base, "2.0.0-1")
assert result.version == "2.0.0-1"
package_mock.assert_called_once_with(package_ahriman.base)
def test_package_load_unknown(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must raise UnknownPackageError if package not found
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[])
with pytest.raises(UnknownPackageError):
Rollback.package_load(application, package_ahriman.base, package_ahriman.version)
@@ -1,37 +0,0 @@
import pytest
from pathlib import Path
from unittest.mock import MagicMock
from ahriman.core.alpm.pacman_handle import PacmanHandle
def test_package_load() -> None:
"""
must load package from archive path
"""
local = Path("local")
instance = PacmanHandle.ephemeral()
handle_mock = instance.handle = MagicMock()
instance.package_load(local)
handle_mock.load_pkg.assert_called_once_with(str(local))
PacmanHandle._ephemeral = None
def test_getattr() -> None:
"""
must proxy attribute access to underlying handle
"""
instance = PacmanHandle.ephemeral()
assert instance.dbpath
def test_getattr_not_found() -> None:
"""
must raise AttributeError for missing handle attributes
"""
instance = PacmanHandle.ephemeral()
with pytest.raises(AttributeError):
assert instance.random_attribute
@@ -1,111 +0,0 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.build_tools.package_version import PackageVersion
from ahriman.core.configuration import Configuration
from ahriman.core.utils import utcnow
from ahriman.models.package import Package
from ahriman.models.pkgbuild import Pkgbuild
def test_actual_version(package_ahriman: Package, configuration: Configuration) -> None:
"""
must return same actual_version as version is
"""
assert PackageVersion(package_ahriman).actual_version(configuration) == package_ahriman.version
def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Configuration,
mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must return valid actual_version for VCS package
"""
pkgbuild = resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild"
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild))
mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
unlink_mock = mocker.patch("pathlib.Path.unlink")
assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == "3.1.r13.g4959b52-1"
init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None)
unlink_mock.assert_called_once_with()
def test_actual_version_failed(package_tpacpi_bat_git: Package, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must return same version in case if exception occurred
"""
mocker.patch("ahriman.core.build_tools.task.Task.init", side_effect=Exception)
mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
unlink_mock = mocker.patch("pathlib.Path.unlink")
assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == package_tpacpi_bat_git.version
unlink_mock.assert_called_once_with()
def test_is_newer_than(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must correctly check if package is newer than specified timestamp
"""
# base checks, true/false
older = package_ahriman.packages[package_ahriman.base].build_date - 1
assert PackageVersion(package_ahriman).is_newer_than(older)
newer = package_ahriman.packages[package_ahriman.base].build_date + 1
assert not PackageVersion(package_ahriman).is_newer_than(newer)
# list check
min_date = min(package.build_date for package in package_python_schedule.packages.values())
assert PackageVersion(package_python_schedule).is_newer_than(min_date)
# null list check
package_python_schedule.packages["python-schedule"].build_date = None
assert PackageVersion(package_python_schedule).is_newer_than(min_date)
package_python_schedule.packages["python2-schedule"].build_date = None
assert not PackageVersion(package_python_schedule).is_newer_than(min_date)
def test_is_outdated_false(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must be not outdated for the same package
"""
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version",
return_value=package_ahriman.version)
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration)
actual_version_mock.assert_called_once_with(configuration)
def test_is_outdated_true(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must be outdated for the new version
"""
other = Package.from_json(package_ahriman.view())
other.version = other.version.replace("-1", "-2")
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version",
return_value=other.version)
assert PackageVersion(package_ahriman).is_outdated(other, configuration)
actual_version_mock.assert_called_once_with(configuration)
def test_is_outdated_no_version_calculation(package_ahriman: Package, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must not call actual version if calculation is disabled
"""
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version")
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration, calculate_version=False)
actual_version_mock.assert_not_called()
def test_is_outdated_fresh_package(package_ahriman: Package, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must not call actual version if package is never than specified time
"""
configuration.set_option("build", "vcs_allowed_age", str(int(utcnow().timestamp())))
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version")
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration)
actual_version_mock.assert_not_called()
@@ -1,8 +0,0 @@
from ahriman.core.database.migrations.m017_pkgbuild import steps
def test_migration_pkgbuild() -> None:
"""
migration must not be empty
"""
assert steps
@@ -1,8 +0,0 @@
from ahriman.core.database.migrations.m018_package_hold import steps
def test_migration_package_hold() -> None:
"""
migration must not be empty
"""
assert steps
@@ -1,32 +0,0 @@
from ahriman.core.formatters import PkgbuildPrinter
from ahriman.models.changes import Changes
def test_properties(pkgbuild_printer: PkgbuildPrinter) -> None:
"""
must return non-empty properties list
"""
assert pkgbuild_printer.properties()
def test_properties_empty() -> None:
"""
must return empty properties list if pkgbuild is empty
"""
assert not PkgbuildPrinter(Changes()).properties()
assert not PkgbuildPrinter(Changes("sha", "changes")).properties()
def test_title(pkgbuild_printer: PkgbuildPrinter) -> None:
"""
must return non-empty title
"""
assert pkgbuild_printer.title()
def test_title_empty() -> None:
"""
must return empty title if change is empty
"""
assert not PkgbuildPrinter(Changes()).title()
assert not PkgbuildPrinter(Changes("sha")).title()
@@ -1,80 +0,0 @@
import pytest
import requests
from pytest_mock import MockerFixture
from ahriman.core.http import SyncAhrimanClient
from ahriman.models.user import User
def test_adapters(ahriman_client: SyncAhrimanClient) -> None:
"""
must return native adapters
"""
assert "http+unix://" not in ahriman_client.adapters()
def test_adapters_unix_socket(ahriman_client: SyncAhrimanClient) -> None:
"""
must register unix socket adapter
"""
ahriman_client.address = "http+unix://path"
assert "http+unix://" in ahriman_client.adapters()
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
"""
must generate login url correctly
"""
assert ahriman_client._login_url().startswith(ahriman_client.address)
assert ahriman_client._login_url().endswith("/api/v1/login")
def test_headers(ahriman_client: SyncAhrimanClient) -> None:
"""
must inject request id header
"""
assert "X-Request-ID" in ahriman_client.headers()
def test_on_session_creation(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must log in user on start
"""
ahriman_client.auth = (user.username, user.password)
requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request")
payload = {
"username": user.username,
"password": user.password
}
session = requests.Session()
ahriman_client.on_session_creation(session)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
def test_on_session_creation_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during session start
"""
ahriman_client.user = user
mocker.patch("requests.Session.request", side_effect=Exception)
ahriman_client.on_session_creation(requests.Session())
def test_start_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during session start
"""
ahriman_client.user = user
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
ahriman_client.on_session_creation(requests.Session())
def test_start_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
"""
must skip login if no user set
"""
requests_mock = mocker.patch("requests.Session.request")
ahriman_client.on_session_creation(requests.Session())
requests_mock.assert_not_called()
@@ -1,73 +0,0 @@
import logging
import pytest
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task
from ahriman.core.database import SQLite
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
def test_logger(database: SQLite, repo: Repo) -> None:
"""
must set logger attribute
"""
assert database.logger
assert database.logger.name == "sql"
assert repo.logger
assert repo.logger.name == "ahriman.core.alpm.repo.Repo"
def test_logger_name(database: SQLite, repo: Repo, task_ahriman: Task) -> None:
"""
must correctly generate logger name
"""
assert database.logger_name == "sql"
assert repo.logger_name == "ahriman.core.alpm.repo.Repo"
assert task_ahriman.logger_name == "ahriman.core.build_tools.task.Task"
def test_in_context(database: SQLite) -> None:
"""
must set and reset generic log context
"""
with database.in_context("package_id", "42"):
record = logging.makeLogRecord({})
assert record.package_id == "42"
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_in_context_failed(database: SQLite) -> None:
"""
must reset context even if exception occurs
"""
with pytest.raises(ValueError):
with database.in_context("package_id", "42"):
raise ValueError()
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_in_package_context(database: SQLite, package_ahriman: Package) -> None:
"""
must set package log context
"""
with database.in_package_context(package_ahriman.base, package_ahriman.version):
record = logging.makeLogRecord({})
assert record.package_id == LogRecordId(package_ahriman.base, package_ahriman.version)
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package) -> None:
"""
must set package log context with empty version
"""
with database.in_package_context(package_ahriman.base, None):
record = logging.makeLogRecord({})
assert record.package_id == LogRecordId(package_ahriman.base, "<unknown>")
@@ -1,75 +0,0 @@
import logging
from ahriman.core.log.log_context import LogContext
def test_get() -> None:
"""
must get context variable value
"""
token = LogContext.set("package_id", "value")
assert LogContext.get("package_id") == "value"
LogContext.reset("package_id", token)
def test_get_empty() -> None:
"""
must return None when context variable is unknown or not set
"""
assert LogContext.get("package_id") is None
assert LogContext.get("random") is None
def test_log_record_factory() -> None:
"""
must inject all registered context variables into log records
"""
package_token = LogContext.set("package_id", "package")
record = logging.makeLogRecord({})
assert record.package_id == "package"
LogContext.reset("package_id", package_token)
def test_log_record_factory_empty() -> None:
"""
must not inject context variable when value is None
"""
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_register() -> None:
"""
must register a context variable
"""
variable = LogContext.register("random")
assert "random" in LogContext._context
assert LogContext._context["random"] is variable
del LogContext._context["random"]
def test_reset() -> None:
"""
must reset context variable so it is no longer injected
"""
token = LogContext.set("package_id", "value")
LogContext.reset("package_id", token)
record = logging.makeLogRecord({})
assert not hasattr(record, "package_id")
def test_set() -> None:
"""
must set context variable and inject it into log records
"""
token = LogContext.set("package_id", "value")
record = logging.makeLogRecord({})
assert record.package_id == "value"
LogContext.reset("package_id", token)
@@ -1,56 +0,0 @@
from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Explorer
from ahriman.models.repository_id import RepositoryId
def test_repositories_extract(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on arguments
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Explorer.repositories_extract(configuration, "repo", "arch") == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
def test_repositories_extract_repository(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on arguments and tree
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value={"repo"})
assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_repository_legacy(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on arguments and tree (legacy mode)
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "aur")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_architecture(configuration: Configuration, mocker: MockerFixture) -> None:
"""
must read repository name from config
"""
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures",
return_value={"arch"})
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Explorer.repositories_extract(configuration, repository="repo") == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo")
known_repositories_mock.assert_not_called()
@@ -1,268 +0,0 @@
import pytest
from dataclasses import replace
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.repository import Repository
from ahriman.models.changes import Changes
from ahriman.models.package import Package
def test_full_depends(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
pyalpm_package_ahriman: MagicMock) -> None:
"""
must extract all dependencies from the package
"""
package_python_schedule.packages[package_python_schedule.base].provides = ["python3-schedule"]
database_mock = MagicMock()
database_mock.pkgcache = [pyalpm_package_ahriman]
repository.pacman = MagicMock()
repository.pacman.handle.get_syncdbs.return_value = [database_mock]
assert repository.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends
package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base]
expected = sorted(set(package_python_schedule.depends + package_ahriman.depends))
assert repository.full_depends(package_python_schedule, [package_python_schedule]) == expected
def test_load_archives(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must return all packages grouped by package base
"""
single_packages = [
Package(base=package_python_schedule.base,
version=package_python_schedule.version,
remote=package_python_schedule.remote,
packages={package: props})
for package, props in package_python_schedule.packages.items()
] + [package_ahriman]
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages)
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[
(package_ahriman, None),
])
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
assert len(packages) == 2
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
archives = sum((list(package.packages.keys()) for package in packages), start=[])
assert len(archives) == 3
expected = set(package_ahriman.packages.keys())
expected.update(package_python_schedule.packages.keys())
assert set(archives) == expected
def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> None:
"""
must skip packages which cannot be loaded
"""
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=Exception)
assert not repository.load_archives([Path("a.pkg.tar.xz")])
def test_load_archives_not_package(repository: Repository) -> None:
"""
must skip not packages from iteration
"""
assert not repository.load_archives([Path("a.tar.xz")])
def test_load_archives_different_version(repository: Repository, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must load packages with different versions choosing maximal
"""
single_packages = [
Package(base=package_python_schedule.base,
version=package_python_schedule.version,
remote=package_python_schedule.remote,
packages={package: props})
for package, props in package_python_schedule.packages.items()
]
single_packages[0].version = "0.0.1-1"
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages)
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
assert len(packages) == 1
assert packages[0].version == package_python_schedule.version
def test_load_archives_all_versions(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must load packages with different versions keeping all when latest_only is False
"""
mocker.patch("ahriman.models.package.Package.from_archive",
side_effect=[package_ahriman, replace(package_ahriman, version="0.0.1-1")])
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[])
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")], latest_only=False)
assert len(packages) == 2
def test_package_archives(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package archives sorted by version
"""
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir")
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[replace(package_ahriman, version=str(i)) for i in range(5)])
result = repository.package_archives(package_ahriman.base)
assert len(result) == 5
assert [p.version for p in result] == [str(i) for i in range(5)]
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), latest_only=False)
def test_package_archives_no_directory(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return empty list if archive directory does not exist
"""
mocker.patch("pathlib.Path.is_dir", return_value=False)
assert repository.package_archives(package_ahriman.base) == []
def test_package_archives_architecture_mismatch(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must skip packages with mismatched architecture
"""
package_ahriman.packages[package_ahriman.base].architecture = "i686"
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.iterdir")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman])
result = repository.package_archives(package_ahriman.base)
assert len(result) == 0
def test_package_archives_lookup(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must existing packages which match the version
"""
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[
package_ahriman,
package_python_schedule,
replace(package_ahriman, version="1"),
])
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("1.pkg.tar.xz")])
assert repository.package_archives_lookup(package_ahriman) == [Path("1.pkg.tar.xz")]
archives_mock.assert_called_once_with(package_ahriman.base)
glob_mock.assert_called_once_with(f"{package_ahriman.packages[package_ahriman.base].filename}*")
def test_package_archives_lookup_version_mismatch(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return nothing if no packages found with the same version
"""
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
return_value=[replace(package_ahriman, version="1")])
assert repository.package_archives_lookup(package_ahriman) == []
def test_package_archives_lookup_architecture_mismatch(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return nothing if architecture doesn't match
"""
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
assert repository.package_archives_lookup(package_ahriman) == []
def test_package_archives_lookup_no_archive_directory(repository: Repository, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must return nothing if no archive directory found
"""
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
assert repository.package_archives_lookup(package_ahriman) == []
def test_package_changes(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must load package changes
"""
changes = Changes("sha", "change")
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2")
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes)
assert repository.package_changes(package_ahriman, changes.last_commit_sha) == changes
load_mock.assert_called_once_with(
pytest.helpers.anyvar(int), package_ahriman, [], repository.configuration.repository_paths)
changes_mock.assert_called_once_with(pytest.helpers.anyvar(int), changes.last_commit_sha)
def test_package_changes_skip(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must skip loading package changes if no new commits
"""
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha")
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes")
assert repository.package_changes(package_ahriman, "sha") is None
changes_mock.assert_not_called()
def test_packages(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must return repository packages
"""
mocker.patch("pathlib.Path.iterdir")
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman, package_python_schedule])
assert repository.packages() == [package_ahriman, package_python_schedule]
# it uses filter object, so we cannot verify argument list =/
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_packages_filter(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must filter result by bases
"""
mocker.patch("pathlib.Path.iterdir")
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
return_value=[package_ahriman, package_python_schedule])
assert repository.packages([package_ahriman.base]) == [package_ahriman]
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
"""
must return build packages
"""
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
assert repository.packages_built() == [Path("b.pkg.tar.xz")]
def test_packages_depend_on(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must filter packages by depends list
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
assert repository.packages_depend_on([package_ahriman], {"python-srcinfo"}) == [package_ahriman]
def test_packages_depend_on_empty(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
must return all packages in case if no filter is provided
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages",
return_value=[package_ahriman, package_python_schedule])
assert repository.packages_depend_on([package_ahriman, package_python_schedule], None) == \
[package_ahriman, package_python_schedule]
@@ -1,145 +0,0 @@
import pytest
from asyncio import QueueShutDown
from ahriman.core.status.event_bus import EventBus
from ahriman.models.event import EventType
from ahriman.models.package import Package
async def test_broadcast(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must broadcast event to all subscribers
"""
_, queue = await event_bus.subscribe()
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base, version=package_ahriman.version)
message = queue.get_nowait()
assert message == (
EventType.PackageUpdated,
{"object_id": package_ahriman.base, "version": package_ahriman.version},
)
async def test_broadcast_with_topics(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must broadcast event to subscribers with matching topics
"""
_, queue = await event_bus.subscribe([EventType.PackageUpdated])
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
assert not queue.empty()
async def test_broadcast_topic_isolation(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must not broadcast event to subscribers with non-matching topics
"""
_, queue = await event_bus.subscribe([EventType.BuildLog])
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
assert queue.empty()
async def test_broadcast_queue_full(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must discard message to slow subscriber
"""
event_bus.max_size = 1
_, queue = await event_bus.subscribe()
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
await event_bus.broadcast(EventType.PackageRemoved, package_ahriman.base)
assert queue.qsize() == 1
async def test_broadcast_queue_shutdown(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must skip subscriber whose queue was shutdown concurrently
"""
_, queue = await event_bus.subscribe()
queue.shutdown()
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
async def test_shutdown(event_bus: EventBus) -> None:
"""
must shutdown all subscriber queues on shutdown
"""
subscriber_id, queue = await event_bus.subscribe()
await event_bus.shutdown()
assert subscriber_id not in event_bus._subscribers
with pytest.raises(QueueShutDown):
queue.get_nowait()
async def test_shutdown_queue_full(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must handle shutdown when queue is full
"""
event_bus.max_size = 1
_, queue = await event_bus.subscribe()
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
await event_bus.shutdown()
async def test_subscribe(event_bus: EventBus) -> None:
"""
must register new subscriber
"""
subscriber_id, queue = await event_bus.subscribe()
assert subscriber_id
assert queue.empty()
assert subscriber_id in event_bus._subscribers
async def test_broadcast_with_object_id(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must broadcast event to subscribers with matching object_id
"""
_, queue = await event_bus.subscribe(object_id=package_ahriman.base)
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
assert not queue.empty()
async def test_broadcast_object_id_isolation(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must not broadcast event to subscribers with non-matching object_id
"""
_, queue = await event_bus.subscribe(object_id="other-package")
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
assert queue.empty()
async def test_subscribe_with_topics(event_bus: EventBus) -> None:
"""
must register subscriber with topic filter
"""
subscriber_id, _ = await event_bus.subscribe([EventType.BuildLog])
assert event_bus._subscribers[subscriber_id].topics == [EventType.BuildLog]
async def test_subscribe_with_object_id(event_bus: EventBus, package_ahriman: Package) -> None:
"""
must register subscriber with object_id filter
"""
subscriber_id, _ = await event_bus.subscribe(object_id=package_ahriman.base)
assert event_bus._subscribers[subscriber_id].object_id == package_ahriman.base
async def test_unsubscribe(event_bus: EventBus) -> None:
"""
must remove subscriber
"""
subscriber_id, _ = await event_bus.subscribe()
await event_bus.unsubscribe(subscriber_id)
assert subscriber_id not in event_bus._subscribers
async def test_unsubscribe_unknown(event_bus: EventBus) -> None:
"""
must not fail on unknown subscriber removal
"""
await event_bus.unsubscribe("unknown")
@@ -1,385 +0,0 @@
import pytest
from pytest_mock import MockerFixture
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.status.watcher import Watcher
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.changes import Changes
from ahriman.models.dependencies import Dependencies
from ahriman.models.event import Event, EventType
from ahriman.models.log_record import LogRecord
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
async def test_event_add(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must create new event
"""
event = Event("event", "object")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_add")
await watcher.event_add(event)
cache_mock.assert_called_once_with(event)
async def test_event_get(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must retrieve events
"""
event = Event("event", "object")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=[event])
result = await watcher.event_get(None, None)
assert result == [event]
cache_mock.assert_called_once_with(None, None, None, None, -1, 0)
async def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must correctly load packages
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
return_value=[(package_ahriman, BuildStatus())])
await watcher.load()
cache_mock.assert_called_once_with(None)
package, status = watcher._known[package_ahriman.base]
assert package == package_ahriman
assert status.status == BuildStatusEnum.Unknown
async def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must correctly load packages with known statuses
"""
status = BuildStatus(BuildStatusEnum.Success)
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[(package_ahriman, status)])
watcher._known = {package_ahriman.base: (package_ahriman, status)}
await watcher.load()
_, status = watcher._known[package_ahriman.base]
assert status.status == BuildStatusEnum.Success
async def test_logs_rotate(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must rotate logs
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.logs_rotate")
await watcher.logs_rotate(42)
cache_mock.assert_called_once_with(42)
async def test_package_archives(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package archives from package info
"""
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
return_value=[package_ahriman])
result = await watcher.package_archives(package_ahriman.base)
assert result == [package_ahriman]
archives_mock.assert_called_once_with(package_ahriman.base)
async def test_package_get(watcher: Watcher, package_ahriman: Package) -> None:
"""
must return package status
"""
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
package, status = await watcher.package_get(package_ahriman.base)
assert package == package_ahriman
assert status.status == BuildStatusEnum.Unknown
async def test_package_get_failed(watcher: Watcher, package_ahriman: Package) -> None:
"""
must fail on unknown package
"""
with pytest.raises(UnknownPackageError):
await watcher.package_get(package_ahriman.base)
async def test_package_changes_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package changes
"""
changes = Changes("sha")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
return_value=changes)
assert await watcher.package_changes_get(package_ahriman.base) == changes
cache_mock.assert_called_once_with(package_ahriman.base)
async def test_package_changes_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update package changes
"""
changes = Changes("sha")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
await watcher.package_changes_update(package_ahriman.base, changes)
cache_mock.assert_called_once_with(package_ahriman.base, changes)
async def test_package_dependencies_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package dependencies
"""
dependencies = Dependencies({"path": [package_ahriman.base]})
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_get",
return_value=dependencies)
assert await watcher.package_dependencies_get(package_ahriman.base) == dependencies
cache_mock.assert_called_once_with(package_ahriman.base)
async def test_package_dependencies_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update package dependencies
"""
dependencies = Dependencies({"path": [package_ahriman.base]})
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_update")
await watcher.package_dependencies_update(package_ahriman.base, dependencies)
cache_mock.assert_called_once_with(package_ahriman.base, dependencies)
async def test_package_hold_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update package hold status
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
await watcher.package_hold_update(package_ahriman.base, enabled=True)
cache_mock.assert_called_once_with(package_ahriman.base, enabled=True)
_, status = watcher._known[package_ahriman.base]
assert status.is_held is True
broadcast_mock.assert_called_once_with(EventType.PackageHeld, package_ahriman.base, is_held=True)
async def test_package_hold_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
"""
must fail on unknown package hold update
"""
with pytest.raises(UnknownPackageError):
await watcher.package_hold_update(package_ahriman.base, enabled=True)
async def test_package_logs_add(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must post log record
"""
log_record = LogRecord(LogRecordId(package_ahriman.base, "1.0.0"), 42.0, "message")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_add")
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
await watcher.package_logs_add(log_record)
cache_mock.assert_called_once_with(log_record)
broadcast_mock.assert_called_once_with(EventType.BuildLog, package_ahriman.base, **log_record.view())
async def test_package_logs_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package logs
"""
log_record = LogRecord(LogRecordId(package_ahriman.base, "1.0.0"), 42.0, "message")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_get",
return_value=[log_record])
assert await watcher.package_logs_get(package_ahriman.base) == [log_record]
cache_mock.assert_called_once_with(package_ahriman.base, None, None, -1, 0)
async def test_package_logs_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must remove package logs
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_remove")
await watcher.package_logs_remove(package_ahriman.base, None)
cache_mock.assert_called_once_with(package_ahriman.base, None)
async def test_package_patches_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must return package patches
"""
patch = PkgbuildPatch("key", "value")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_get", return_value=[patch])
assert await watcher.package_patches_get(package_ahriman.base, None) == [patch]
cache_mock.assert_called_once_with(package_ahriman.base, None)
async def test_package_patches_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must remove package patches
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_remove")
await watcher.package_patches_remove(package_ahriman.base, None)
cache_mock.assert_called_once_with(package_ahriman.base, None)
async def test_package_patches_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update package patches
"""
patch = PkgbuildPatch("key", "value")
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_update")
await watcher.package_patches_update(package_ahriman.base, patch)
cache_mock.assert_called_once_with(package_ahriman.base, patch)
async def test_package_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must remove package base
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
await watcher.package_remove(package_ahriman.base)
assert not watcher._known
cache_mock.assert_called_once_with(package_ahriman.base)
broadcast_mock.assert_called_once_with(EventType.PackageRemoved, package_ahriman.base)
async def test_package_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must not fail on unknown base removal
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
await watcher.package_remove(package_ahriman.base)
cache_mock.assert_called_once_with(package_ahriman.base)
broadcast_mock.assert_called_once_with(EventType.PackageRemoved, package_ahriman.base)
async def test_package_status_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must update package status only for known package
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_status_update")
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
await watcher.package_status_update(package_ahriman.base, BuildStatusEnum.Success)
cache_mock.assert_called_once_with(package_ahriman.base, pytest.helpers.anyvar(int))
package, status = watcher._known[package_ahriman.base]
assert package == package_ahriman
assert status.status == BuildStatusEnum.Success
broadcast_mock.assert_called_once_with(
EventType.PackageStatusChanged, package_ahriman.base, status=BuildStatusEnum.Success.value,
)
async def test_package_status_update_preserves_hold(watcher: Watcher, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must preserve hold status on package status update
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_status_update")
mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus(is_held=True))}
await watcher.package_status_update(package_ahriman.base, BuildStatusEnum.Success)
_, status = watcher._known[package_ahriman.base]
assert status.is_held is True
async def test_package_status_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
"""
must fail on unknown package status update only
"""
with pytest.raises(UnknownPackageError):
await watcher.package_status_update(package_ahriman.base, BuildStatusEnum.Unknown)
async def test_package_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must add package to cache
"""
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_update")
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
await watcher.package_update(package_ahriman, BuildStatusEnum.Unknown)
assert await watcher.packages()
cache_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
broadcast_mock.assert_called_once_with(
EventType.PackageUpdated, package_ahriman.base,
status=BuildStatusEnum.Unknown.value, version=package_ahriman.version,
)
async def test_package_update_preserves_hold(watcher: Watcher, package_ahriman: Package,
mocker: MockerFixture) -> None:
"""
must preserve hold status on package update
"""
mocker.patch("ahriman.core.status.local_client.LocalClient.package_update")
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus(is_held=True))}
await watcher.package_update(package_ahriman, BuildStatusEnum.Success)
_, status = watcher._known[package_ahriman.base]
assert status.is_held is True
async def test_packages(watcher: Watcher, package_ahriman: Package) -> None:
"""
must return list of available packages
"""
assert not await watcher.packages()
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
assert await watcher.packages()
async def test_shutdown(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must gracefully shutdown watcher
"""
shutdown_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.shutdown")
await watcher.shutdown()
shutdown_mock.assert_called_once_with()
async def test_status_update(watcher: Watcher, mocker: MockerFixture) -> None:
"""
must update service status
"""
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
await watcher.status_update(BuildStatusEnum.Success)
assert watcher.status.status == BuildStatusEnum.Success
broadcast_mock.assert_called_once_with(EventType.ServiceStatusChanged, None, status=BuildStatusEnum.Success.value)
def test_call(watcher: Watcher, package_ahriman: Package) -> None:
"""
must return self instance if package exists
"""
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
assert watcher(package_ahriman.base)
def test_call_skip(watcher: Watcher) -> None:
"""
must return self instance if no package base set
"""
assert watcher(None)
def test_call_failed(watcher: Watcher, package_ahriman: Package) -> None:
"""
must raise UnknownPackage
"""
with pytest.raises(UnknownPackageError):
assert watcher(package_ahriman.base)
-162
View File
@@ -1,162 +0,0 @@
import datetime
import pytest
from unittest.mock import MagicMock, PropertyMock
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR
from ahriman.core.configuration import Configuration
from ahriman.models.aur_package import AURPackage
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.remote_source import RemoteSource
from ahriman.models.scan_paths import ScanPaths
@pytest.fixture
def aur_package_akonadi() -> AURPackage:
"""
fixture for AUR package
Returns:
AURPackage: AUR package test instance
"""
return AURPackage(
id=0,
name="akonadi",
package_base_id=0,
package_base="akonadi",
version="21.12.3-2",
description="PIM layer, which provides an asynchronous API to access all kind of PIM data",
num_votes=0,
popularity=0.0,
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
last_modified=datetime.datetime.fromtimestamp(1646555990.610, datetime.UTC),
url_path="",
url="https://kontact.kde.org",
out_of_date=None,
maintainer="felixonmars",
repository="extra",
depends=[
"libakonadi",
"mariadb",
],
make_depends=[
"boost",
"doxygen",
"extra-cmake-modules",
"kaccounts-integration",
"kitemmodels",
"postgresql",
"qt5-tools",
],
opt_depends=[
"postgresql: PostgreSQL backend",
],
conflicts=[],
provides=[],
license=["LGPL"],
keywords=[],
groups=[],
)
@pytest.fixture
def package_tpacpi_bat_git() -> Package:
"""
git package fixture
Returns:
Package: git package test instance
"""
return Package(
base="tpacpi-bat-git",
version="3.1.r12.g4959b52-1",
remote=RemoteSource(
source=PackageSource.AUR,
git_url=AUR.remote_git_url("tpacpi-bat-git", "aur"),
web_url=AUR.remote_web_url("tpacpi-bat-git"),
path=".",
branch="master",
),
packages={"tpacpi-bat-git": PackageDescription()})
@pytest.fixture
def pacman(configuration: Configuration) -> Pacman:
"""
fixture for pacman wrapper
Args:
configuration(Configuration): configuration fixture
Returns:
Pacman: pacman wrapper test instance
"""
_, repository_id = configuration.check_loaded()
return Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
@pytest.fixture
def passwd() -> MagicMock:
"""
get passwd structure for the user
Returns:
MagicMock: passwd structure test instance
"""
passwd = MagicMock()
passwd.pw_dir = "home"
passwd.pw_name = "ahriman"
return passwd
@pytest.fixture
def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
"""
mock object for pyalpm package
Args:
aur_package_ahriman(AURPackage): package fixture
Returns:
MagicMock: pyalpm package mock
"""
mock = MagicMock()
db = type(mock).db = MagicMock()
type(mock).base = PropertyMock(return_value=aur_package_ahriman.package_base)
type(mock).builddate = PropertyMock(
return_value=aur_package_ahriman.last_modified.replace(tzinfo=datetime.timezone.utc).timestamp())
type(mock).conflicts = PropertyMock(return_value=aur_package_ahriman.conflicts)
type(db).name = PropertyMock(return_value="aur")
type(mock).depends = PropertyMock(return_value=aur_package_ahriman.depends)
type(mock).desc = PropertyMock(return_value=aur_package_ahriman.description)
type(mock).licenses = PropertyMock(return_value=aur_package_ahriman.license)
type(mock).makedepends = PropertyMock(return_value=aur_package_ahriman.make_depends)
type(mock).name = PropertyMock(return_value=aur_package_ahriman.name)
type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends)
type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends)
type(mock).packager = PropertyMock(return_value="packager")
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)
type(mock).groups = PropertyMock(return_value=aur_package_ahriman.groups)
return mock
@pytest.fixture
def scan_paths(configuration: Configuration) -> ScanPaths:
"""
scan paths fixture
Args:
configuration(Configuration): configuration test instance
Returns:
ScanPaths: scan paths test instance
"""
return ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
-32
View File
@@ -1,32 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ahriman-triggers"
description = "ArcH linux ReposItory MANager, additional extensions"
readme = "../README.md"
requires-python = ">=3.13"
license = {file = "../COPYING"}
authors = [
{name = "ahriman team"},
]
dependencies = [
"ahriman-core",
]
dynamic = ["version"]
[project.urls]
Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases"
[tool.hatch.version]
path = "../ahriman-core/src/ahriman/__init__.py"
[tool.hatch.build.targets.wheel]
only-include = ["src/ahriman"]
sources = ["src"]
[tool.hatch.build.targets.wheel.shared-data]
"package/share" = "share"
View File
@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ahriman</title>
<link rel="icon" href="/static/favicon.ico" />
<script type="module" crossorigin src="/static/index.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
-55
View File
@@ -1,55 +0,0 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ahriman-web"
description = "ArcH linux ReposItory MANager, web server"
readme = "../README.md"
requires-python = ">=3.13"
license = {file = "../COPYING"}
authors = [
{name = "ahriman team"},
]
dependencies = [
"ahriman-core",
"aiohttp",
"aiohttp_cors",
"aiohttp_jinja2",
"aiohttp_sse",
]
dynamic = ["version"]
[project.optional-dependencies]
auth = [
"aiohttp_session",
"aiohttp_security",
"cryptography",
]
docs = [
"aiohttp-apispec",
"setuptools",
]
metrics = [
"aiohttp-openmetrics",
]
oauth2 = [
"ahriman-web[auth]",
"aioauth-client",
]
[project.urls]
Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases"
[tool.hatch.version]
path = "../ahriman-core/src/ahriman/__init__.py"
[tool.hatch.build.targets.wheel]
only-include = ["src/ahriman"]
sources = ["src"]
[tool.hatch.build.targets.wheel.shared-data]
"package/lib" = "lib"
"package/share" = "share"
@@ -1,61 +0,0 @@
#
# Copyright (c) 2021-2026 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 hashlib
from aiohttp import ETag
from aiohttp.typedefs import Middleware
from aiohttp.web import HTTPNotModified, Request, Response, StreamResponse, middleware
from ahriman.web.middlewares import HandlerType
__all__ = ["etag_handler"]
def etag_handler() -> Middleware:
"""
middleware to handle ETag header for conditional requests. It computes ETag from the response body
and returns 304 Not Modified if the client sends a matching ``If-None-Match`` header
Returns:
Middleware: built middleware
Raises:
HTTPNotModified: if content matches ``If-None-Match`` header sent
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
response = await handler(request)
if not isinstance(response, Response) or not isinstance(response.body, bytes):
return response
if request.method not in ("GET", "HEAD"):
return response
etag = ETag(value=hashlib.md5(response.body, usedforsecurity=False).hexdigest())
response.etag = etag
if request.if_none_match is not None and etag in request.if_none_match:
raise HTTPNotModified(headers={"ETag": response.headers["ETag"]})
return response
return handle
@@ -1,51 +0,0 @@
#
# Copyright (c) 2021-2026 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 uuid
from aiohttp.typedefs import Middleware
from aiohttp.web import Request, StreamResponse, middleware
from ahriman.core.log.log_context import LogContext
from ahriman.web.middlewares import HandlerType
__all__ = ["request_id_handler"]
def request_id_handler() -> Middleware:
"""
middleware to trace request id header
Returns:
Middleware: request id processing middleware
"""
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
request_id = request.headers.getone("X-Request-ID", str(uuid.uuid4()))
token = LogContext.set("request_id", request_id)
try:
response = await handler(request)
response.headers["X-Request-ID"] = request_id
return response
finally:
LogContext.reset("request_id", token)
return handle
@@ -1,41 +0,0 @@
#
# Copyright (c) 2021-2026 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.apispec import Schema, fields
class AuthInfoSchema(Schema):
"""
authorization information schema
"""
control = fields.String(
metadata={
"description": "HTML control for login interface",
"example": "<button type=\"button\" class=\"btn btn-link\" data-bs-toggle=\"modal\" data-bs-target=\"#login-modal\" style=\"text-decoration: none\"><i class=\"bi bi-box-arrow-in-right\"></i> login</button>",
})
enabled = fields.Boolean(required=True, metadata={
"description": "Whether authentication is enabled or not",
})
external = fields.Boolean(required=True, metadata={
"description": "Whether authorization provider is external (e.g. OAuth)",
})
username = fields.String(metadata={
"description": "Currently authenticated username if available",
})
@@ -1,38 +0,0 @@
#
# Copyright (c) 2021-2026 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.apispec import Schema, fields
class AutoRefreshIntervalSchema(Schema):
"""
auto refresh interval schema
"""
interval = fields.Integer(required=True, metadata={
"description": "Auto refresh interval in milliseconds",
"example": "60000",
})
is_active = fields.Boolean(required=True, metadata={
"description": "Whether this interval is the default active one",
})
text = fields.String(required=True, metadata={
"description": "Human readable interval description",
"example": "1 minute",
})
@@ -1,37 +0,0 @@
#
# Copyright (c) 2021-2026 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.models.event import EventType
from ahriman.web.apispec import fields
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class EventBusFilterSchema(RepositoryIdSchema):
"""
request event bus filter schema
"""
event = fields.List(fields.String(), metadata={
"description": "Event type filter",
"example": [EventType.PackageUpdated],
})
object_id = fields.String(metadata={
"description": "Object identifier filter",
"example": "ahriman",
})
@@ -1,30 +0,0 @@
#
# Copyright (c) 2021-2026 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.apispec import Schema, fields
class HoldSchema(Schema):
"""
request hold schema
"""
is_held = fields.Boolean(required=True, metadata={
"description": "Package hold status",
})
@@ -1,51 +0,0 @@
#
# Copyright (c) 2021-2026 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 import __version__
from ahriman.web.apispec import Schema, fields
from ahriman.web.schemas.auth_info_schema import AuthInfoSchema
from ahriman.web.schemas.auto_refresh_interval_schema import AutoRefreshIntervalSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class InfoV2Schema(Schema):
"""
response service information schema
"""
auth = fields.Nested(AuthInfoSchema(), required=True, metadata={
"description": "Authorization descriptor",
})
autorefresh_intervals = fields.Nested(AutoRefreshIntervalSchema(many=True), metadata={
"description": "Available auto refresh intervals",
})
docs_enabled = fields.Boolean(metadata={
"description": "Whether API documentation is enabled",
})
index_url = fields.String(metadata={
"description": "URL to the repository index page",
"example": "https://ahriman.readthedocs.io/",
})
repositories = fields.Nested(RepositoryIdSchema(many=True), required=True, metadata={
"description": "List of loaded repositories",
})
version = fields.String(required=True, metadata={
"description": "Service version",
"example": __version__,
})
@@ -1,30 +0,0 @@
#
# Copyright (c) 2021-2026 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.apispec import Schema, fields
class PackagerSchema(Schema):
"""
request packager schema
"""
packager = fields.String(metadata={
"description": "Packager identity if applicable",
})
@@ -1,40 +0,0 @@
#
# Copyright (c) 2021-2026 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 import __version__
from ahriman.web.apispec import fields
from ahriman.web.schemas.packager_schema import PackagerSchema
class RollbackSchema(PackagerSchema):
"""
request schema for package rollback
"""
hold = fields.Boolean(dump_default=True, metadata={
"description": "Hold package after rollback",
})
package = fields.String(required=True, metadata={
"description": "Package name",
"example": "ahriman",
})
version = fields.String(required=True, metadata={
"description": "Package version",
"example": __version__,
})
@@ -1,35 +0,0 @@
#
# Copyright (c) 2021-2026 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.models.event import EventType
from ahriman.web.apispec import Schema, fields
class SSESchema(Schema):
"""
response SSE schema
"""
event = fields.String(required=True, metadata={
"description": "Event type",
"example": EventType.PackageUpdated,
})
data = fields.Dict(keys=fields.String(), values=fields.Raw(), metadata={
"description": "Event data",
})
@@ -1,73 +0,0 @@
#
# Copyright (c) 2021-2026 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 collections.abc import Callable
from typing import Any
from ahriman import __version__
from ahriman.core.auth.helpers import authorized_userid
from ahriman.core.types import Comparable
from ahriman.core.utils import pretty_interval
from ahriman.web.apispec import aiohttp_apispec
from ahriman.web.views.base import BaseView
__all__ = ["server_info"]
async def server_info(view: BaseView) -> dict[str, Any]:
"""
generate server info which can be used in responses directly
Args:
view(BaseView): view of the request
Returns:
dict[str, Any]: server info as a JSON response
"""
autorefresh_intervals = [
{
"interval": interval * 1000, # milliseconds
"is_active": index == 0, # first element is always default
"text": pretty_interval(interval),
}
for index, interval in enumerate(view.configuration.getintlist("web", "autorefresh_intervals", fallback=[]))
if interval > 0 # special case if 0 exists and first, refresh will not be turned on by default
]
comparator: Callable[[dict[str, Any]], Comparable] = lambda interval: interval["interval"]
return {
"auth": {
"control": view.validator.auth_control,
"enabled": view.validator.enabled,
"external": view.validator.is_external,
"username": await authorized_userid(view.request),
},
"autorefresh_intervals": sorted(autorefresh_intervals, key=comparator),
"docs_enabled": aiohttp_apispec is not None,
"index_url": view.configuration.get("web", "index_url", fallback=None),
"repositories": [
{
"id": repository_id.id,
**repository_id.view(),
}
for repository_id in sorted(view.services)
],
"version": __version__,
}
@@ -1,164 +0,0 @@
#
# Copyright (c) 2021-2026 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 json
from aiohttp.web import HTTPBadRequest, Request, Response, StreamResponse
from aiohttp_sse import EventSourceResponse, sse_response
from asyncio import Queue, QueueShutDown, wait_for
from typing import ClassVar
from ahriman.core.status.event_bus import SSEvent
from ahriman.models.event import EventType
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import EventBusFilterSchema, SSESchema
from ahriman.web.views.base import BaseView
class EventBusView(BaseView):
"""
event bus SSE view
Attributes:
READ_EVENTS(set[EventType]): (class attribute) events which are allowed for read-only users
"""
READ_EVENTS: ClassVar[set[EventType]] = {
EventType.PackageHeld,
EventType.PackageOutdated,
EventType.PackageRemoved,
EventType.PackageStatusChanged,
EventType.PackageUpdateFailed,
EventType.PackageUpdated,
EventType.ServiceStatusChanged,
}
ROUTES = ["/api/v1/events/stream"]
@classmethod
async def get_permission(cls, request: Request) -> UserAccess:
"""
retrieve user permission from the request
Args:
request(Request): request object
Returns:
UserAccess: extracted permission
"""
if request.method.upper() not in ("GET", "HEAD"):
return await BaseView.get_permission(request)
permission = UserAccess.Full
try:
topics = cls(request)._topics()
except HTTPBadRequest:
topics = None
if topics is not None and set(topics).issubset(cls.READ_EVENTS):
permission = UserAccess.Read
return permission
@staticmethod
async def _run(response: EventSourceResponse, queue: Queue[SSEvent]) -> None:
"""
read events from queue and send them to the client
Args:
response(EventSourceResponse): SSE response instance
queue(Queue[SSEvent]): subscriber queue
"""
while response.is_connected():
try:
event_type, data = await wait_for(queue.get(), timeout=response.ping_interval)
except TimeoutError:
continue
except QueueShutDown:
break
await response.send(json.dumps(data), event=event_type)
def _topics(self) -> list[EventType] | None:
"""
parse event filter from request query
Returns:
list[EventType] | None: event filter if any
Raises:
HTTPBadRequest: if invalid event type is supplied
"""
if self.request.query is None:
return None
try:
return [EventType(event) for event in self.request.query.getall("event", [])] or None
except ValueError as ex:
raise HTTPBadRequest(reason=str(ex))
@apidocs(
tags=["Audit log"],
summary="Live updates",
description="Stream live updates via SSE. Read-only users may subscribe only when all requested event filters "
"belong to read-safe package and service status events; build log or unfiltered streams require "
"full access. Streams are live-only and do not replay missed events after reconnect.",
permission=UserAccess.Full,
error_400_enabled=True,
error_404_description="Repository is unknown",
schema=SSESchema(many=True),
query_schema=EventBusFilterSchema,
)
async def get(self) -> StreamResponse:
"""
subscribe on updates
Returns:
StreamResponse: 200 with streaming updates
"""
topics = self._topics()
object_id = self.request.query.get("object_id")
event_bus = self.service().event_bus
async with sse_response(self.request) as response:
subscription_id, queue = await event_bus.subscribe(topics, object_id=object_id)
try:
await self._run(response, queue)
except (ConnectionResetError, QueueShutDown):
pass
finally:
await event_bus.unsubscribe(subscription_id)
return response
async def head(self) -> StreamResponse:
"""
HEAD method implementation based on the result of GET method
Returns:
StreamResponse: generated response for the request
"""
self._topics()
self.service()
return Response(headers={
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
})
@@ -1,65 +0,0 @@
#
# Copyright (c) 2021-2026 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 aiohttp.web import Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import PackageNameSchema, PackageSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
class Archives(StatusViewGuard, BaseView):
"""
package archives web view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/archives"]
@apidocs(
tags=["Packages"],
summary="Get package archives",
description="Retrieve built package archives for the base",
permission=GET_PERMISSION,
error_404_description="Package base and/or repository are unknown",
schema=PackageSchema(many=True),
match_schema=PackageNameSchema,
query_schema=RepositoryIdSchema,
)
async def get(self) -> Response:
"""
get package archives
Returns:
Response: 200 with package archives on success
Raises:
HTTPNotFound: if no package was found
"""
package_base = self.request.match_info["package"]
archives = await self.service(package_base=package_base).package_archives(package_base)
return self.json_response([archive.view() for archive in archives])
@@ -1,75 +0,0 @@
#
# Copyright (c) 2021-2026 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 aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound
from typing import ClassVar
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import HoldSchema, PackageNameSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
class HoldView(StatusViewGuard, BaseView):
"""
package hold web view
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/hold"]
@apidocs(
tags=["Packages"],
summary="Update package hold status",
description="Set package hold status",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Package base and/or repository are unknown",
match_schema=PackageNameSchema,
query_schema=RepositoryIdSchema,
body_schema=HoldSchema,
)
async def post(self) -> None:
"""
update package hold status
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
HTTPNotFound: if no package was found
"""
package_base = self.request.match_info["package"]
try:
data = await self.request.json()
is_held = data["is_held"]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
try:
await self.service().package_hold_update(package_base, enabled=is_held)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
raise HTTPNoContent
@@ -1,77 +0,0 @@
#
# Copyright (c) 2021-2026 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 aiohttp.web import HTTPBadRequest, Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import ProcessIdSchema, RepositoryIdSchema, RollbackSchema
from ahriman.web.views.base import BaseView
class RollbackView(BaseView):
"""
package rollback web view
Attributes:
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/service/rollback"]
@apidocs(
tags=["Actions"],
summary="Rollback package",
description="Rollback package to specified version",
permission=POST_PERMISSION,
error_400_enabled=True,
schema=ProcessIdSchema,
query_schema=RepositoryIdSchema,
body_schema=RollbackSchema,
)
async def post(self) -> Response:
"""
run package rollback
Returns:
Response: 200 with spawned process id
Raises:
HTTPBadRequest: if bad data is supplied
"""
try:
data = await self.request.json()
package = self.get_non_empty(lambda key: data[key], "package")
version = self.get_non_empty(lambda key: data[key], "version")
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_rollback(
repository_id,
package,
version,
username,
hold=data.get("hold", True),
)
return self.json_response({"process_id": process_id})
@@ -1,19 +0,0 @@
#
# Copyright (c) 2021-2026 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/>.
#
@@ -1,19 +0,0 @@
#
# Copyright (c) 2021-2026 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/>.
#
@@ -1,19 +0,0 @@
#
# Copyright (c) 2021-2026 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/>.
#
@@ -1,56 +0,0 @@
#
# Copyright (c) 2021-2026 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 aiohttp.web import Response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import InfoV2Schema
from ahriman.web.server_info import server_info
from ahriman.web.views.base import BaseView
class InfoView(BaseView):
"""
web service information view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/api/v2/info"]
@apidocs(
tags=["Status"],
summary="Service information",
description="Perform basic service health check and returns its information",
permission=GET_PERMISSION,
schema=InfoV2Schema,
)
async def get(self) -> Response:
"""
get service information
Returns:
Response: 200 with service information object
"""
response = await server_info(self)
return self.json_response(response)
@@ -1,85 +0,0 @@
import hashlib
import pytest
from aiohttp import ETag
from aiohttp.web import HTTPNotModified, Response, StreamResponse
from unittest.mock import AsyncMock
from ahriman.web.middlewares.etag_handler import etag_handler
async def test_etag_handler() -> None:
"""
must set ETag header on GET responses
"""
request = pytest.helpers.request("", "", "GET")
request.if_none_match = None
request_handler = AsyncMock(return_value=Response(body=b"hello"))
handler = etag_handler()
result = await handler(request, request_handler)
assert result.etag is not None
async def test_etag_handler_not_modified() -> None:
"""
must raise NotModified when ETag matches If-None-Match
"""
body = b"hello"
request = pytest.helpers.request("", "", "GET")
request.if_none_match = (ETag(value=hashlib.md5(body, usedforsecurity=False).hexdigest()),)
request_handler = AsyncMock(return_value=Response(body=body))
handler = etag_handler()
with pytest.raises(HTTPNotModified):
await handler(request, request_handler)
async def test_etag_handler_no_match() -> None:
"""
must return full response when ETag does not match If-None-Match
"""
request = pytest.helpers.request("", "", "GET")
request.if_none_match = (ETag(value="outdated"),)
request_handler = AsyncMock(return_value=Response(body=b"hello"))
handler = etag_handler()
result = await handler(request, request_handler)
assert result.status == 200
assert result.etag is not None
async def test_etag_handler_skip_post() -> None:
"""
must skip ETag for non-GET/HEAD methods
"""
request = pytest.helpers.request("", "", "POST")
request_handler = AsyncMock(return_value=Response(body=b"hello"))
handler = etag_handler()
result = await handler(request, request_handler)
assert result.etag is None
async def test_etag_handler_skip_no_body() -> None:
"""
must skip ETag for responses without body
"""
request = pytest.helpers.request("", "", "GET")
request_handler = AsyncMock(return_value=Response())
handler = etag_handler()
result = await handler(request, request_handler)
assert result.etag is None
async def test_etag_handler_skip_stream() -> None:
"""
must skip ETag for streaming responses
"""
request = pytest.helpers.request("", "", "GET")
request_handler = AsyncMock(return_value=StreamResponse())
handler = etag_handler()
result = await handler(request, request_handler)
assert "ETag" not in result.headers
@@ -1,43 +0,0 @@
import logging
import pytest
from unittest.mock import AsyncMock, MagicMock
from typing import Any
from ahriman.web.middlewares.request_id_handler import request_id_handler
async def test_request_id_handler() -> None:
"""
must use request id from request if available
"""
request = pytest.helpers.request("", "", "")
request.headers = MagicMock()
request.headers.getone.return_value = "request_id"
response = MagicMock()
response.headers = {}
async def check_handler(_: Any) -> MagicMock:
record = logging.makeLogRecord({})
assert record.request_id == "request_id"
return response
handler = request_id_handler()
await handler(request, check_handler)
assert response.headers["X-Request-ID"] == "request_id"
async def test_request_id_handler_generate() -> None:
"""
must generate request id and set it in response header
"""
request = pytest.helpers.request("", "", "")
response = MagicMock()
response.headers = {}
request_handler = AsyncMock(return_value=response)
handler = request_id_handler()
await handler(request, request_handler)
assert "X-Request-ID" in response.headers
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1 +0,0 @@
# schema testing goes in view class tests
@@ -1,55 +0,0 @@
import aiohttp_cors
import pytest
from aiohttp.web import Application
from pytest_mock import MockerFixture
from ahriman.web.cors import setup_cors
from ahriman.web.keys import ConfigurationKey
def test_setup_cors(application: Application) -> None:
"""
must setup CORS
"""
cors = application[aiohttp_cors.APP_CONFIG_KEY]
# let's test here that it is enabled for all requests
for route in application.router.routes():
# we don't want to deal with match info here though
try:
url = route.url_for()
except (KeyError, TypeError):
continue
request = pytest.helpers.request(application, url, route.method, resource=route.resource)
assert cors._cors_impl._router_adapter.is_cors_enabled_on_request(request)
def test_setup_cors_custom_origins(application: Application, mocker: MockerFixture) -> None:
"""
must setup CORS with custom origins
"""
configuration = application[ConfigurationKey]
configuration.set_option("web", "cors_allow_origins", "https://example.com https://httpbin.com")
setup_mock = mocker.patch("ahriman.web.cors.aiohttp_cors.setup", return_value=mocker.MagicMock())
setup_cors(application, configuration)
defaults = setup_mock.call_args.kwargs["defaults"]
assert "https://example.com" in defaults
assert "https://httpbin.com" in defaults
assert "*" not in defaults
def test_setup_cors_custom_methods(application: Application, mocker: MockerFixture) -> None:
"""
must setup CORS with custom methods
"""
configuration = application[ConfigurationKey]
configuration.set_option("web", "cors_allow_methods", "GET POST")
setup_mock = mocker.patch("ahriman.web.cors.aiohttp_cors.setup", return_value=mocker.MagicMock())
setup_cors(application, configuration)
defaults = setup_mock.call_args.kwargs["defaults"]
resource_options = next(iter(defaults.values()))
assert resource_options.allow_methods == {"GET", "POST"}
@@ -1,27 +0,0 @@
import pytest
from aiohttp.web import Application
from ahriman import __version__
from ahriman.models.repository_id import RepositoryId
from ahriman.web.server_info import server_info
from ahriman.web.views.index import IndexView
async def test_server_info(application: Application, repository_id: RepositoryId) -> None:
"""
must generate server info
"""
request = pytest.helpers.request(application, "", "GET")
view = IndexView(request)
result = await server_info(view)
assert result["repositories"] == [{"id": repository_id.id, **repository_id.view()}]
assert not result["auth"]["enabled"]
assert not result["auth"]["external"]
assert result["auth"]["username"] is None
assert result["auth"]["control"]
assert result["version"] == __version__
assert result["autorefresh_intervals"] == []
assert result["docs_enabled"]
assert result["index_url"] is None
@@ -1,254 +0,0 @@
import asyncio
import pytest
from aiohttp.test_utils import TestClient
from aiohttp.web import HTTPBadRequest
from asyncio import Queue
from multidict import MultiDict
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.core.status.watcher import Watcher
from ahriman.models.event import EventType
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.keys import WatcherKey
from ahriman.web.views.base import BaseView
from ahriman.web.views.v1.auditlog.event_bus import EventBusView
async def _producer(watcher: Watcher, package_ahriman: Package) -> None:
"""
create producer
Args:
watcher(Watcher): watcher test instance
package_ahriman(Package): package test instance
"""
await asyncio.sleep(0.1)
await watcher.event_bus.broadcast(EventType.PackageRemoved, package_ahriman.base)
await watcher.event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base, status="success")
await asyncio.sleep(0.1)
await watcher.event_bus.shutdown()
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET",):
request = pytest.helpers.request("", "", method)
assert await EventBusView.get_permission(request) == UserAccess.Full
async def test_get_permission_build_log() -> None:
"""
must return full permission for build log stream
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict(event=EventType.BuildLog))
assert await EventBusView.get_permission(request) == UserAccess.Full
async def test_get_permission_build_log_with_read_events() -> None:
"""
must return full permission for mixed build log and read event stream
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict([
("event", EventType.BuildLog),
("event", EventType.PackageUpdated),
]))
assert await EventBusView.get_permission(request) == UserAccess.Full
async def test_get_permission_invalid_event() -> None:
"""
must return full permission for invalid event type
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict(event="invalid"))
assert await EventBusView.get_permission(request) == UserAccess.Full
async def test_get_permission_post() -> None:
"""
must use default permission for non-get requests
"""
request = pytest.helpers.request("", "", "POST", params=MultiDict(event=EventType.PackageUpdated))
assert await EventBusView.get_permission(request) == await BaseView.get_permission(request)
async def test_get_permission_read_events() -> None:
"""
must return read permission for package and status streams
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict(
("event", event_type) for event_type in EventBusView.READ_EVENTS
))
assert await EventBusView.get_permission(request) == UserAccess.Read
def test_routes() -> None:
"""
must return correct routes
"""
assert EventBusView.ROUTES == ["/api/v1/events/stream"]
async def test_run_timeout() -> None:
"""
must handle timeout and continue loop
"""
queue = Queue()
async def _shutdown() -> None:
await asyncio.sleep(0.05)
queue.shutdown()
response = AsyncMock()
response.is_connected = lambda: True
response.ping_interval = 0.01
asyncio.create_task(_shutdown())
await EventBusView._run(response, queue)
def test_topics() -> None:
"""
must parse event filters
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict([
("event", EventType.PackageUpdated),
("event", EventType.PackageRemoved),
]))
assert EventBusView(request)._topics() == [EventType.PackageUpdated, EventType.PackageRemoved]
def test_topics_empty() -> None:
"""
must return None for missing event filters
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict())
assert EventBusView(request)._topics() is None
def test_topics_invalid() -> None:
"""
must raise bad request for invalid event filters
"""
request = pytest.helpers.request("", "", "GET", params=MultiDict(event="invalid"))
with pytest.raises(HTTPBadRequest):
EventBusView(request)._topics()
async def test_get(client: TestClient, package_ahriman: Package) -> None:
"""
must stream events via SSE
"""
watcher = next(iter(client.app[WatcherKey].values()))
asyncio.create_task(_producer(watcher, package_ahriman))
request_schema = pytest.helpers.schema_request(EventBusView.get, location="querystring")
# no content validation here because it is a streaming response
assert not request_schema.validate({})
response = await client.get("/api/v1/events/stream")
assert response.status == 200
body = await response.text()
assert EventType.PackageUpdated in body
assert "ahriman" in body
async def test_get_with_topic_filter(client: TestClient, package_ahriman: Package) -> None:
"""
must filter events by topic
"""
watcher = next(iter(client.app[WatcherKey].values()))
asyncio.create_task(_producer(watcher, package_ahriman))
request_schema = pytest.helpers.schema_request(EventBusView.get, location="querystring")
payload = {"event": [EventType.PackageUpdated]}
assert not request_schema.validate(payload)
response = await client.get("/api/v1/events/stream", params=payload)
assert response.status == 200
body = await response.text()
assert EventType.PackageUpdated in body
assert EventType.PackageRemoved not in body
async def test_get_with_object_id_filter(client: TestClient, package_ahriman: Package) -> None:
"""
must filter events by object_id
"""
watcher = next(iter(client.app[WatcherKey].values()))
asyncio.create_task(_producer(watcher, package_ahriman))
request_schema = pytest.helpers.schema_request(EventBusView.get, location="querystring")
payload = {"object_id": "non-existent-package"}
assert not request_schema.validate(payload)
response = await client.get("/api/v1/events/stream", params=payload)
assert response.status == 200
body = await response.text()
assert "ahriman" not in body
async def test_get_bad_request(client: TestClient) -> None:
"""
must return bad request for invalid event type
"""
response_schema = pytest.helpers.schema_response(EventBusView.get, code=400)
response = await client.get("/api/v1/events/stream", params={"event": "invalid"})
assert response.status == 400
assert not response_schema.validate(await response.json())
async def test_get_not_found(client: TestClient) -> None:
"""
must return not found for unknown repository
"""
response_schema = pytest.helpers.schema_response(EventBusView.get, code=404)
response = await client.get("/api/v1/events/stream", params={"architecture": "unknown", "repository": "unknown"})
assert response.status == 404
assert not response_schema.validate(await response.json())
async def test_get_connection_reset(client: TestClient, mocker: MockerFixture) -> None:
"""
must handle connection reset
"""
mocker.patch.object(EventBusView, "_run", side_effect=ConnectionResetError)
response = await client.get("/api/v1/events/stream")
assert response.status == 200
async def test_head(client: TestClient) -> None:
"""
must check stream availability without opening SSE stream
"""
response = await client.head("/api/v1/events/stream", params={"event": EventType.PackageUpdated})
assert response.status == 200
assert response.headers["Content-Type"] == "text/event-stream"
assert not await response.text()
async def test_head_bad_request(client: TestClient) -> None:
"""
must return bad request for invalid event type
"""
response = await client.head("/api/v1/events/stream", params={"event": "invalid"})
assert response.status == 400
assert not await response.text()
async def test_head_not_found(client: TestClient) -> None:
"""
must return not found for unknown repository
"""
response = await client.head("/api/v1/events/stream", params={"architecture": "unknown", "repository": "unknown"})
assert response.status == 404
assert not await response.text()
@@ -1,52 +0,0 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.packages.archives import Archives
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET",):
request = pytest.helpers.request("", "", method)
assert await Archives.get_permission(request) == UserAccess.Reporter
def test_routes() -> None:
"""
must return correct routes
"""
assert Archives.ROUTES == ["/api/v1/packages/{package}/archives"]
async def test_get(client: TestClient, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must get archives for package
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
mocker.patch("ahriman.core.status.watcher.Watcher.package_archives", return_value=[package_ahriman])
response_schema = pytest.helpers.schema_response(Archives.get)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/archives")
assert response.status == 200
archives = await response.json()
assert not response_schema.validate(archives)
async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None:
"""
must return not found for missing package
"""
response_schema = pytest.helpers.schema_response(Archives.get, code=404)
response = await client.get(f"/api/v1/packages/{package_ahriman.base}/archives")
assert response.status == 404
assert not response_schema.validate(await response.json())
@@ -1,60 +0,0 @@
import pytest
from aiohttp.test_utils import TestClient
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.packages.hold import HoldView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await HoldView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert HoldView.ROUTES == ["/api/v1/packages/{package}/hold"]
async def test_post(client: TestClient, package_ahriman: Package) -> None:
"""
must update package hold status
"""
await client.post(f"/api/v1/packages/{package_ahriman.base}",
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
request_schema = pytest.helpers.schema_request(HoldView.post)
payload = {"is_held": True}
assert not request_schema.validate(payload)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/hold", json=payload)
assert response.status == 204
async def test_post_not_found(client: TestClient, package_ahriman: Package) -> None:
"""
must return Not Found for unknown package
"""
response_schema = pytest.helpers.schema_response(HoldView.post, code=404)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/hold", json={"is_held": False})
assert response.status == 404
assert not response_schema.validate(await response.json())
async def test_post_exception(client: TestClient, package_ahriman: Package) -> None:
"""
must raise exception on invalid payload
"""
response_schema = pytest.helpers.schema_response(HoldView.post, code=400)
response = await client.post(f"/api/v1/packages/{package_ahriman.base}/hold", json=[])
assert response.status == 400
assert not response_schema.validate(await response.json())
@@ -1,70 +0,0 @@
import pytest
from aiohttp.test_utils import TestClient
from pytest_mock import MockerFixture
from unittest.mock import AsyncMock
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v1.service.rollback import RollbackView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("POST",):
request = pytest.helpers.request("", "", method)
assert await RollbackView.get_permission(request) == UserAccess.Full
def test_routes() -> None:
"""
must return correct routes
"""
assert RollbackView.ROUTES == ["/api/v1/service/rollback"]
async def test_post(client: TestClient, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must call post request correctly
"""
rollback_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rollback", return_value="abc")
user_mock = AsyncMock()
user_mock.return_value = "username"
mocker.patch("ahriman.web.views.base.BaseView.username", side_effect=user_mock)
request_schema = pytest.helpers.schema_request(RollbackView.post)
response_schema = pytest.helpers.schema_response(RollbackView.post)
payload = {"package": "ahriman", "version": "version"}
assert not request_schema.validate(payload)
response = await client.post("/api/v1/service/rollback", json=payload)
assert response.ok
rollback_mock.assert_called_once_with(repository_id, "ahriman", "version", "username", hold=True)
json = await response.json()
assert json["process_id"] == "abc"
assert not response_schema.validate(json)
async def test_post_empty(client: TestClient, mocker: MockerFixture) -> None:
"""
must call raise 400 on empty request
"""
rollback_mock = mocker.patch("ahriman.core.spawn.Spawn.packages_rollback")
response_schema = pytest.helpers.schema_response(RollbackView.post, code=400)
response = await client.post("/api/v1/service/rollback", json={"package": "", "version": "version"})
assert response.status == 400
assert not response_schema.validate(await response.json())
rollback_mock.assert_not_called()
response = await client.post("/api/v1/service/rollback", json={"package": "ahriman", "version": ""})
assert response.status == 400
assert not response_schema.validate(await response.json())
rollback_mock.assert_not_called()
response = await client.post("/api/v1/service/rollback", json={})
assert response.status == 400
assert not response_schema.validate(await response.json())
rollback_mock.assert_not_called()
@@ -1,43 +0,0 @@
import pytest
from aiohttp.test_utils import TestClient
from ahriman import __version__
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
from ahriman.web.views.v2.status.info import InfoView
async def test_get_permission() -> None:
"""
must return correct permission for the request
"""
for method in ("GET",):
request = pytest.helpers.request("", "", method)
assert await InfoView.get_permission(request) == UserAccess.Unauthorized
def test_routes() -> None:
"""
must return correct routes
"""
assert InfoView.ROUTES == ["/api/v2/info"]
async def test_get(client: TestClient, repository_id: RepositoryId) -> None:
"""
must return service information
"""
response_schema = pytest.helpers.schema_response(InfoView.get)
response = await client.get("/api/v2/info")
assert response.ok
json = await response.json()
assert not response_schema.validate(json)
assert json["repositories"] == [{"id": repository_id.id, **repository_id.view()}]
assert not json["auth"]["enabled"]
assert json["auth"]["control"]
assert json["version"] == __version__
assert json["autorefresh_intervals"] == []
assert json["docs_enabled"]
-18
View File
@@ -1,18 +0,0 @@
import pytest
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
@pytest.fixture
def auth(configuration: Configuration) -> Auth:
"""
auth provider fixture
Args:
configuration(Configuration): configuration fixture
Returns:
Auth: auth service instance
"""
return Auth(configuration)
+14 -17
View File
@@ -1,15 +1,13 @@
# build image
FROM archlinux:base AS build
ARG BUILD_DATE
# install environment
## create build user
RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build
## extract container creation date and set mirror for this timestamp, set PKGEXT and refresh database next
RUN echo "Server = https://archive.archlinux.org/repos/${BUILD_DATE//-/\/}/\$repo/os/\$arch" > "/etc/pacman.d/mirrorlist" && \
pacman -Syyuu --noconfirm
RUN echo "Server = https://archive.archlinux.org/repos/$(stat -c "%y" "/var/lib/pacman" | cut -d " " -f 1 | sed "s,-,/,g")/\$repo/os/\$arch" > "/etc/pacman.d/mirrorlist" && \
pacman -Sy
## setup package cache
RUN runuser -u build -- mkdir "/tmp/pkg" && \
echo "PKGDEST=/tmp/pkg" >> "/etc/makepkg.conf" && \
@@ -25,22 +23,21 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
RUN pacman -S --noconfirm --asdeps \
devtools \
git \
npm \
pyalpm \
python-bcrypt \
python-filelock \
python-bcrypt \
python-inflection \
python-pyelftools \
python-requests
RUN pacman -S --noconfirm --asdeps \
python-requests \
&& \
pacman -S --noconfirm --asdeps \
base-devel \
python-build \
python-hatchling \
python-flit \
python-installer \
python-setuptools \
python-tox \
python-wheel
RUN pacman -S --noconfirm --asdeps \
python-wheel \
&& \
pacman -S --noconfirm --asdeps \
git \
python-aiohttp \
python-aiohttp-openmetrics \
@@ -49,8 +46,9 @@ RUN pacman -S --noconfirm --asdeps \
python-cryptography \
python-jinja \
python-systemd \
rsync
RUN runuser -u build -- install-aur-package \
rsync \
&& \
runuser -u build -- install-aur-package \
python-aioauth-client \
python-sphinx-typlog-theme \
python-webargs \
@@ -59,7 +57,6 @@ RUN runuser -u build -- install-aur-package \
python-aiohttp-jinja2 \
python-aiohttp-session \
python-aiohttp-security \
python-aiohttp-sse-git \
python-requests-unixsocket2
# install ahriman
@@ -110,7 +107,7 @@ RUN cp "/etc/pacman.d/mirrorlist" "/etc/pacman.d/mirrorlist.orig" && \
echo "Server = file:///var/cache/pacman/pkg" > "/etc/pacman.d/mirrorlist" && \
cp "/etc/pacman.conf" "/etc/pacman.conf.orig" && \
sed -i "s/SigLevel *=.*/SigLevel = Optional/g" "/etc/pacman.conf" && \
pacman -Syyuu --noconfirm
pacman -Sy
## install package and its optional dependencies
RUN pacman -S --noconfirm ahriman
RUN pacman -S --noconfirm --asdeps \
+1 -3
View File
@@ -7,10 +7,8 @@ for PACKAGE in "$@"; do
# clone the remote source
git clone https://aur.archlinux.org/"$PACKAGE".git "$BUILD_DIR"
cd "$BUILD_DIR"
# FIXME monkey patch PKGBUILD for python
sed -i 's/python -m build/python -m build --skip-dependency-check/g' "PKGBUILD"
# checkout to the image date
git checkout "$(git rev-list -1 --before="$BUILD_DATE" master)"
git checkout "$(git rev-list -1 --before="$(stat -c "%y" "/var/lib/pacman" | cut -d " " -f 1)" master)"
# build and install the package
makepkg --nocheck --noconfirm --install --rmdeps --syncdeps
cd /
+1421 -1561
View File
File diff suppressed because it is too large Load Diff
-32
View File
@@ -12,14 +12,6 @@ ahriman.application.handlers.add module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.archives module
--------------------------------------------
.. automodule:: ahriman.application.handlers.archives
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.backup module
------------------------------------------
@@ -84,14 +76,6 @@ ahriman.application.handlers.help module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.hold module
----------------------------------------
.. automodule:: ahriman.application.handlers.hold
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.key\_import module
-----------------------------------------------
@@ -108,14 +92,6 @@ ahriman.application.handlers.patch module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.pkgbuild module
--------------------------------------------
.. automodule:: ahriman.application.handlers.pkgbuild
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.rebuild module
-------------------------------------------
@@ -164,14 +140,6 @@ ahriman.application.handlers.restore module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.rollback module
--------------------------------------------
.. automodule:: ahriman.application.handlers.rollback
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.run module
---------------------------------------
-8
View File
@@ -28,14 +28,6 @@ ahriman.core.alpm.pacman\_database module
:no-undoc-members:
:show-inheritance:
ahriman.core.alpm.pacman\_handle module
---------------------------------------
.. automodule:: ahriman.core.alpm.pacman_handle
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.alpm.pkgbuild\_parser module
-----------------------------------------
-8
View File
@@ -12,14 +12,6 @@ ahriman.core.build\_tools.package\_archive module
:no-undoc-members:
:show-inheritance:
ahriman.core.build\_tools.package\_version module
-------------------------------------------------
.. automodule:: ahriman.core.build_tools.package_version
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.build\_tools.sources module
----------------------------------------
-16
View File
@@ -140,22 +140,6 @@ ahriman.core.database.migrations.m016\_archive module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m017\_pkgbuild module
------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m017_pkgbuild
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m018\_package\_hold module
-----------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m018_package_hold
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
-8
View File
@@ -76,14 +76,6 @@ ahriman.core.formatters.patch\_printer module
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.pkgbuild\_printer module
------------------------------------------------
.. automodule:: ahriman.core.formatters.pkgbuild_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.printer module
--------------------------------------
-8
View File
@@ -28,14 +28,6 @@ ahriman.core.log.lazy\_logging module
:no-undoc-members:
:show-inheritance:
ahriman.core.log.log\_context module
------------------------------------
.. automodule:: ahriman.core.log.log_context
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.log.log\_loader module
-----------------------------------
-8
View File
@@ -28,14 +28,6 @@ ahriman.core.repository.executor module
:no-undoc-members:
:show-inheritance:
ahriman.core.repository.explorer module
---------------------------------------
.. automodule:: ahriman.core.repository.explorer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.repository.package\_info module
--------------------------------------------
-8
View File
@@ -12,14 +12,6 @@ ahriman.core.status.client module
:no-undoc-members:
:show-inheritance:
ahriman.core.status.event\_bus module
-------------------------------------
.. automodule:: ahriman.core.status.event_bus
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.status.local\_client module
----------------------------------------
-16
View File
@@ -12,14 +12,6 @@ ahriman.web.middlewares.auth\_handler module
:no-undoc-members:
:show-inheritance:
ahriman.web.middlewares.etag\_handler module
--------------------------------------------
.. automodule:: ahriman.web.middlewares.etag_handler
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.middlewares.exception\_handler module
-------------------------------------------------
@@ -36,14 +28,6 @@ ahriman.web.middlewares.metrics\_handler module
:no-undoc-members:
:show-inheritance:
ahriman.web.middlewares.request\_id\_handler module
---------------------------------------------------
.. automodule:: ahriman.web.middlewares.request_id_handler
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
-8
View File
@@ -39,14 +39,6 @@ ahriman.web.routes module
:no-undoc-members:
:show-inheritance:
ahriman.web.server\_info module
-------------------------------
.. automodule:: ahriman.web.server_info
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.web module
----------------------
-64
View File
@@ -20,14 +20,6 @@ ahriman.web.schemas.aur\_package\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.auth\_info\_schema module
---------------------------------------------
.. automodule:: ahriman.web.schemas.auth_info_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.auth\_schema module
---------------------------------------
@@ -36,14 +28,6 @@ ahriman.web.schemas.auth\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.auto\_refresh\_interval\_schema module
----------------------------------------------------------
.. automodule:: ahriman.web.schemas.auto_refresh_interval_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.build\_options\_schema module
-------------------------------------------------
@@ -92,14 +76,6 @@ ahriman.web.schemas.error\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.event\_bus\_filter\_schema module
-----------------------------------------------------
.. automodule:: ahriman.web.schemas.event_bus_filter_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.event\_schema module
----------------------------------------
@@ -124,14 +100,6 @@ ahriman.web.schemas.file\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.hold\_schema module
---------------------------------------
.. automodule:: ahriman.web.schemas.hold_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.info\_schema module
---------------------------------------
@@ -140,14 +108,6 @@ ahriman.web.schemas.info\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.info\_v2\_schema module
-------------------------------------------
.. automodule:: ahriman.web.schemas.info_v2_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.internal\_status\_schema module
---------------------------------------------------
@@ -260,14 +220,6 @@ ahriman.web.schemas.package\_version\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.packager\_schema module
-------------------------------------------
.. automodule:: ahriman.web.schemas.packager_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.pagination\_schema module
---------------------------------------------
@@ -348,14 +300,6 @@ ahriman.web.schemas.repository\_stats\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.rollback\_schema module
-------------------------------------------
.. automodule:: ahriman.web.schemas.rollback_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.search\_schema module
-----------------------------------------
@@ -364,14 +308,6 @@ ahriman.web.schemas.search\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.sse\_schema module
--------------------------------------
.. automodule:: ahriman.web.schemas.sse_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.status\_schema module
-----------------------------------------
-8
View File
@@ -4,14 +4,6 @@ ahriman.web.views.v1.auditlog package
Submodules
----------
ahriman.web.views.v1.auditlog.event\_bus module
-----------------------------------------------
.. automodule:: ahriman.web.views.v1.auditlog.event_bus
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.auditlog.events module
-------------------------------------------
-16
View File
@@ -4,14 +4,6 @@ ahriman.web.views.v1.packages package
Submodules
----------
ahriman.web.views.v1.packages.archives module
---------------------------------------------
.. automodule:: ahriman.web.views.v1.packages.archives
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.changes module
--------------------------------------------
@@ -28,14 +20,6 @@ ahriman.web.views.v1.packages.dependencies module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.hold module
-----------------------------------------
.. automodule:: ahriman.web.views.v1.packages.hold
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.packages.logs module
-----------------------------------------
-8
View File
@@ -68,14 +68,6 @@ ahriman.web.views.v1.service.request module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.rollback module
--------------------------------------------
.. automodule:: ahriman.web.views.v1.service.rollback
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.search module
------------------------------------------
-1
View File
@@ -8,7 +8,6 @@ Subpackages
:maxdepth: 4
ahriman.web.views.v2.packages
ahriman.web.views.v2.status
Module contents
---------------

Some files were not shown because too many files have changed in this diff Show More