diff --git a/Makefile b/Makefile
index e2138a20..380d82c9 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: archive archive_directory archlinux check clean directory push version
+.PHONY: archive archive_directory archlinux check clean directory push tests version
.DEFAULT_GOAL := archlinux
PROJECT := ahriman
@@ -16,21 +16,21 @@ archive: archive_directory
archive_directory: $(TARGET_FILES)
rm -fr $(addprefix $(PROJECT)/, $(IGNORE_FILES))
- find $(PROJECT) -type f -name '*.pyc' -delete
- find $(PROJECT) -depth -type d -name '__pycache__' -execdir rm -rf {} +
- find $(PROJECT) -depth -type d -name '*.egg-info' -execdir rm -rf {} +
+ find "$(PROJECT)" -type f -name "*.pyc" -delete
+ find "$(PROJECT)" -depth -type d -name "__pycache__" -execdir rm -rf {} +
+ find "$(PROJECT)" -depth -type d -name "*.egg-info" -execdir rm -rf {} +
archlinux: archive
sed -i "/sha512sums=('[0-9A-Fa-f]*/s/[^'][^)]*/sha512sums=('$$(sha512sum $(PROJECT)-$(VERSION)-src.tar.xz | awk '{print $$1}')'/" package/archlinux/PKGBUILD
sed -i "s/pkgver=[0-9.]*/pkgver=$(VERSION)/" package/archlinux/PKGBUILD
check:
- cd src && mypy --implicit-reexport --strict -p $(PROJECT)
- cd src && find $(PROJECT) -name '*.py' -execdir autopep8 --max-line-length 120 -aa -i {} +
- cd src && pylint --rcfile=../.pylintrc $(PROJECT)
+ cd src && mypy --implicit-reexport --strict -p "$(PROJECT)"
+ find "src/$(PROJECT)" tests -name "*.py" -execdir autopep8 --exit-code --max-line-length 120 -aa -i {} +
+ cd src && pylint --rcfile=../.pylintrc "$(PROJECT)"
clean:
- find . -type f -name '$(PROJECT)-*-src.tar.xz' -delete
+ find . -type f -name "$(PROJECT)-*-src.tar.xz" -delete
rm -rf "$(PROJECT)"
directory: clean
@@ -43,8 +43,11 @@ push: archlinux
git tag "$(VERSION)"
git push --tags
+tests:
+ python setup.py test
+
version:
ifndef VERSION
$(error VERSION is required, but not set)
endif
- sed -i "/__version__ = '[0-9.]*/s/[^'][^)]*/__version__ = '$(VERSION)'/" src/ahriman/version.py
+ sed -i '/__version__ = "[0-9.]*/s/[^"][^)]*/__version__ = "$(VERSION)"/' src/ahriman/version.py
diff --git a/package/archlinux/PKGBUILD b/package/archlinux/PKGBUILD
index 72b50581..a4e2bce8 100644
--- a/package/archlinux/PKGBUILD
+++ b/package/archlinux/PKGBUILD
@@ -23,7 +23,7 @@ optdepends=('aws-cli: sync to s3'
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgname-$pkgver-src.tar.xz"
'ahriman.sysusers'
'ahriman.tmpfiles')
-sha512sums=('a1db44390ce1785da3d535e3cfd2242d8d56070228eb9b3c1d5629163b65941d60753c481c0fdc69e475e534a828ceea39568dc6711abeee092616dac08e31a9'
+sha512sums=('ed1ef5ee9a2fb25ee1220acb4e7ac30eec0783375766f7ca8c812e1aa84e28d8426e382c1ec3d5357f1141ff683f9dd346970fe1f8bb0c7e29373ee55e478ef4'
'13718afec2c6786a18f0b223ef8e58dccf0688bca4cdbe203f14071f5031ed20120eb0ce38b52c76cfd6e8b6581a9c9eaa2743eb11abbaca637451a84c33f075'
'55b20f6da3d66e7bbf2add5d95a3b60632df121717d25a993e56e737d14f51fe063eb6f1b38bd81cc32e05db01c0c1d80aaa720c45cde87f238d8b46cdb8cbc4')
backup=('etc/ahriman.ini'
diff --git a/package/etc/ahriman.ini b/package/etc/ahriman.ini
index 68f8a965..bb31ce99 100644
--- a/package/etc/ahriman.ini
+++ b/package/etc/ahriman.ini
@@ -42,6 +42,4 @@ remote =
bucket =
[web]
-host =
-port =
templates = /usr/share/ahriman
\ No newline at end of file
diff --git a/package/etc/ahriman.ini.d/logging.ini b/package/etc/ahriman.ini.d/logging.ini
index 42c58d0e..fedc4897 100644
--- a/package/etc/ahriman.ini.d/logging.ini
+++ b/package/etc/ahriman.ini.d/logging.ini
@@ -17,19 +17,19 @@ args = (sys.stderr,)
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
-args = ('/var/log/ahriman/ahriman.log', 'a', 20971520, 20)
+args = ("/var/log/ahriman/ahriman.log", "a", 20971520, 20)
[handler_build_file_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
-args = ('/var/log/ahriman/build.log', 'a', 20971520, 20)
+args = ("/var/log/ahriman/build.log", "a", 20971520, 20)
[handler_http_handler]
class = logging.handlers.RotatingFileHandler
level = DEBUG
formatter = generic_format
-args = ('/var/log/ahriman/http.log', 'a', 20971520, 20)
+args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
[formatter_generic_format]
format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..f3e013e1
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[aliases]
+test = pytest
+
+[tool:pytest]
+addopts = --cov=ahriman --pspec
diff --git a/setup.py b/setup.py
index 8d45c432..54831b34 100644
--- a/setup.py
+++ b/setup.py
@@ -4,69 +4,75 @@ from os import path
here = path.abspath(path.dirname(__file__))
metadata = dict()
-with open(convert_path('src/ahriman/version.py')) as metadata_file:
+with open(convert_path("src/ahriman/version.py")) as metadata_file:
exec(metadata_file.read(), metadata)
setup(
- name='ahriman',
+ name="ahriman",
- version=metadata['__version__'],
+ version=metadata["__version__"],
zip_safe=False,
- description='ArcHlinux ReposItory MANager',
+ description="ArcHlinux ReposItory MANager",
- author='arcanis',
- author_email='',
- url='https://github.com/arcan1s/ahriman',
+ author="arcanis",
+ author_email="",
+ url="https://github.com/arcan1s/ahriman",
- license='GPL3',
+ license="GPL3",
- packages=find_packages('src'),
- package_dir={'': 'src'},
+ packages=find_packages("src"),
+ package_dir={"": "src"},
dependency_links=[
],
install_requires=[
- 'aur',
- 'pyalpm',
- 'srcinfo',
+ "aur",
+ "pyalpm",
+ "srcinfo",
],
setup_requires=[
- 'pytest-runner',
+ "pytest-runner",
],
tests_require=[
- 'pytest',
+ "pytest",
+ "pytest-aiohttp",
+ "pytest-cov",
+ "pytest-helpers-namespace",
+ "pytest-mock",
+ "pytest-pspec",
+ "pytest-resource-path",
],
include_package_data=True,
scripts=[
- 'package/bin/ahriman',
+ "package/bin/ahriman",
],
data_files=[
- ('/etc', [
- 'package/etc/ahriman.ini',
+ ("/etc", [
+ "package/etc/ahriman.ini",
]),
- ('/etc/ahriman.ini.d', [
- 'package/etc/ahriman.ini.d/logging.ini',
+ ("/etc/ahriman.ini.d", [
+ "package/etc/ahriman.ini.d/logging.ini",
]),
- ('lib/systemd/system', [
- 'package/lib/systemd/system/ahriman@.service',
- 'package/lib/systemd/system/ahriman@.timer',
- 'package/lib/systemd/system/ahriman-web@.service',
+ ("lib/systemd/system", [
+ "package/lib/systemd/system/ahriman@.service",
+ "package/lib/systemd/system/ahriman@.timer",
+ "package/lib/systemd/system/ahriman-web@.service",
]),
- ('share/ahriman', [
- 'package/share/ahriman/build-status.jinja2',
- 'package/share/ahriman/repo-index.jinja2',
- 'package/share/ahriman/search.jinja2',
- 'package/share/ahriman/search-line.jinja2',
- 'package/share/ahriman/sorttable.jinja2',
- 'package/share/ahriman/style.jinja2',
+ ("share/ahriman", [
+ "package/share/ahriman/build-status.jinja2",
+ "package/share/ahriman/repo-index.jinja2",
+ "package/share/ahriman/search.jinja2",
+ "package/share/ahriman/search-line.jinja2",
+ "package/share/ahriman/sorttable.jinja2",
+ "package/share/ahriman/style.jinja2",
]),
],
extras_require={
- 'html-templates': ['Jinja2'],
- 'test': ['coverage', 'pytest'],
- 'web': ['Jinja2', 'aiohttp', 'aiohttp_jinja2', 'requests'],
+ "html-templates": ["Jinja2"],
+ "test": ["pytest", "pytest-cov", "pytest-helpers-namespace", "pytest-mock", "pytest-pspec", "pytest-resource-path"],
+ "web": ["Jinja2", "aiohttp", "aiohttp_jinja2", "requests"],
},
)
diff --git a/src/ahriman/__init__.py b/src/ahriman/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/__init__.py
+++ b/src/ahriman/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/application/__init__.py b/src/ahriman/application/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/application/__init__.py
+++ b/src/ahriman/application/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py
index f19dcef6..4fc5d79f 100644
--- a/src/ahriman/application/ahriman.py
+++ b/src/ahriman/application/ahriman.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -24,82 +24,90 @@ import ahriman.application.handlers as handlers
import ahriman.version as version
-if __name__ == '__main__':
- parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager')
+# pylint: disable=too-many-statements
+def _parser() -> argparse.ArgumentParser:
+ """
+ command line parser generator
+ :return: command line parser for the application
+ """
+ parser = argparse.ArgumentParser(prog="ahriman", description="ArcHlinux ReposItory MANager")
parser.add_argument(
- '-a',
- '--architecture',
- help='target architectures (can be used multiple times)',
- action='append')
- parser.add_argument('-c', '--config', help='configuration path', default='/etc/ahriman.ini')
- parser.add_argument('--force', help='force run, remove file lock', action='store_true')
- parser.add_argument('--lock', help='lock file', default='/tmp/ahriman.lock')
- parser.add_argument('--no-log', help='redirect all log messages to stderr', action='store_true')
- parser.add_argument('--no-report', help='force disable reporting to web service', action='store_true')
- parser.add_argument('--unsafe', help='allow to run ahriman as non-ahriman user', action='store_true')
- parser.add_argument('-v', '--version', action='version', version=version.__version__)
- subparsers = parser.add_subparsers(title='command')
+ "-a",
+ "--architecture",
+ help="target architectures (can be used multiple times)",
+ action="append",
+ required=True)
+ parser.add_argument("-c", "--config", help="configuration path", default="/etc/ahriman.ini")
+ parser.add_argument("--force", help="force run, remove file lock", action="store_true")
+ parser.add_argument("--lock", help="lock file", default="/tmp/ahriman.lock")
+ parser.add_argument("--no-log", help="redirect all log messages to stderr", action="store_true")
+ parser.add_argument("--no-report", help="force disable reporting to web service", action="store_true")
+ parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user", action="store_true")
+ parser.add_argument("-v", "--version", action="version", version=version.__version__)
+ subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
- add_parser = subparsers.add_parser('add', description='add package')
- add_parser.add_argument('package', help='package base/name or archive path', nargs='+')
- add_parser.add_argument('--without-dependencies', help='do not add dependencies', action='store_true')
+ add_parser = subparsers.add_parser("add", description="add package")
+ add_parser.add_argument("package", help="package base/name or archive path", nargs="+")
+ add_parser.add_argument("--without-dependencies", help="do not add dependencies", action="store_true")
add_parser.set_defaults(handler=handlers.Add)
- check_parser = subparsers.add_parser('check', description='check for updates. Same as update --dry-run --no-manual')
- check_parser.add_argument('package', help='filter check by package base', nargs='*')
- check_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
+ check_parser = subparsers.add_parser("check", description="check for updates. Same as update --dry-run --no-manual")
+ check_parser.add_argument("package", help="filter check by package base", nargs="*")
+ check_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
check_parser.set_defaults(handler=handlers.Update, no_aur=False, no_manual=True, dry_run=True)
- clean_parser = subparsers.add_parser('clean', description='clear all local caches')
- clean_parser.add_argument('--no-build', help='do not clear directory with package sources', action='store_true')
- clean_parser.add_argument('--no-cache', help='do not clear directory with package caches', action='store_true')
- clean_parser.add_argument('--no-chroot', help='do not clear build chroot', action='store_true')
+ clean_parser = subparsers.add_parser("clean", description="clear all local caches")
+ clean_parser.add_argument("--no-build", help="do not clear directory with package sources", action="store_true")
+ clean_parser.add_argument("--no-cache", help="do not clear directory with package caches", action="store_true")
+ clean_parser.add_argument("--no-chroot", help="do not clear build chroot", action="store_true")
clean_parser.add_argument(
- '--no-manual',
- help='do not clear directory with manually added packages',
- action='store_true')
- clean_parser.add_argument('--no-packages', help='do not clear directory with built packages', action='store_true')
+ "--no-manual",
+ help="do not clear directory with manually added packages",
+ action="store_true")
+ clean_parser.add_argument("--no-packages", help="do not clear directory with built packages", action="store_true")
clean_parser.set_defaults(handler=handlers.Clean)
- config_parser = subparsers.add_parser('config', description='dump configuration for specified architecture')
+ config_parser = subparsers.add_parser("config", description="dump configuration for specified architecture")
config_parser.set_defaults(handler=handlers.Dump, lock=None, no_report=True, unsafe=True)
- rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository')
+ rebuild_parser = subparsers.add_parser("rebuild", description="rebuild whole repository")
rebuild_parser.set_defaults(handler=handlers.Rebuild)
- remove_parser = subparsers.add_parser('remove', description='remove package')
- remove_parser.add_argument('package', help='package name or base', nargs='+')
+ remove_parser = subparsers.add_parser("remove", description="remove package")
+ remove_parser.add_argument("package", help="package name or base", nargs="+")
remove_parser.set_defaults(handler=handlers.Remove)
- report_parser = subparsers.add_parser('report', description='generate report')
- report_parser.add_argument('target', help='target to generate report', nargs='*')
+ report_parser = subparsers.add_parser("report", description="generate report")
+ report_parser.add_argument("target", help="target to generate report", nargs="*")
report_parser.set_defaults(handler=handlers.Report)
- status_parser = subparsers.add_parser('status', description='request status of the package')
- status_parser.add_argument('--ahriman', help='get service status itself', action='store_true')
- status_parser.add_argument('package', help='filter status by package base', nargs='*')
+ status_parser = subparsers.add_parser("status", description="request status of the package")
+ status_parser.add_argument("--ahriman", help="get service status itself", action="store_true")
+ status_parser.add_argument("package", help="filter status by package base", nargs="*")
status_parser.set_defaults(handler=handlers.Status, lock=None, no_report=True, unsafe=True)
- sync_parser = subparsers.add_parser('sync', description='sync packages to remote server')
- sync_parser.add_argument('target', help='target to sync', nargs='*')
+ sync_parser = subparsers.add_parser("sync", description="sync packages to remote server")
+ sync_parser.add_argument("target", help="target to sync", nargs="*")
sync_parser.set_defaults(handler=handlers.Sync)
- update_parser = subparsers.add_parser('update', description='run updates')
- update_parser.add_argument('package', help='filter check by package base', nargs='*')
+ update_parser = subparsers.add_parser("update", description="run updates")
+ update_parser.add_argument("package", help="filter check by package base", nargs="*")
update_parser.add_argument(
- '--dry-run', help='just perform check for updates, same as check command', action='store_true')
- update_parser.add_argument('--no-aur', help='do not check for AUR updates. Implies --no-vcs', action='store_true')
- update_parser.add_argument('--no-manual', help='do not include manual updates', action='store_true')
- update_parser.add_argument('--no-vcs', help='do not check VCS packages', action='store_true')
+ "--dry-run", help="just perform check for updates, same as check command", action="store_true")
+ update_parser.add_argument("--no-aur", help="do not check for AUR updates. Implies --no-vcs", action="store_true")
+ update_parser.add_argument("--no-manual", help="do not include manual updates", action="store_true")
+ update_parser.add_argument("--no-vcs", help="do not check VCS packages", action="store_true")
update_parser.set_defaults(handler=handlers.Update)
- web_parser = subparsers.add_parser('web', description='start web server')
+ web_parser = subparsers.add_parser("web", description="start web server")
web_parser.set_defaults(handler=handlers.Web, lock=None, no_report=True)
- args = parser.parse_args()
- if 'handler' not in args:
- parser.print_help()
- sys.exit(1)
+ return parser
+
+
+if __name__ == "__main__":
+ arg_parser = _parser()
+ args = arg_parser.parse_args()
handler: handlers.Handler = args.handler
status = handler.execute(args)
diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py
index 56c5ef95..8869e2e7 100644
--- a/src/ahriman/application/application.py
+++ b/src/ahriman/application/application.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -18,9 +18,9 @@
# along with this program. If not, see .
#
import logging
-import os
import shutil
+from pathlib import Path
from typing import Callable, Iterable, List, Optional, Set
from ahriman.core.build_tools.task import Task
@@ -32,30 +32,30 @@ from ahriman.models.package import Package
class Application:
- '''
+ """
base application class
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar logger: application logger
:ivar repository: repository instance
- '''
+ """
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
- self.logger = logging.getLogger('root')
+ """
+ self.logger = logging.getLogger("root")
self.config = config
self.architecture = architecture
self.repository = Repository(architecture, config)
def _known_packages(self) -> Set[str]:
- '''
+ """
load packages from repository and pacman repositories
:return: list of known packages
- '''
+ """
known_packages: Set[str] = set()
# local set
for package in self.repository.packages():
@@ -64,15 +64,15 @@ class Application:
return known_packages
def _finalize(self) -> None:
- '''
+ """
generate report and sync to remote server
- '''
+ """
self.report()
self.sync()
def get_updates(self, filter_packages: List[str], no_aur: bool, no_manual: bool, no_vcs: bool,
log_fn: Callable[[str], None]) -> List[Package]:
- '''
+ """
get list of packages to run update process
:param filter_packages: do not check every package just specified in the list
:param no_aur: do not check for aur updates
@@ -80,7 +80,7 @@ class Application:
:param no_vcs: do not check VCS packages
:param log_fn: logger function to log updates
:return: list of out-of-dated packages
- '''
+ """
updates = []
if not no_aur:
@@ -89,60 +89,60 @@ class Application:
updates.extend(self.repository.updates_manual())
for package in updates:
- log_fn(f'{package.base} = {package.version}')
+ log_fn(f"{package.base} = {package.version}")
return updates
def add(self, names: Iterable[str], without_dependencies: bool) -> None:
- '''
+ """
add packages for the next build
:param names: list of package bases to add
:param without_dependencies: if set, dependency check will be disabled
- '''
+ """
known_packages = self._known_packages()
- def add_directory(path: str) -> None:
- for package in filter(package_like, os.listdir(path)):
- full_path = os.path.join(path, package)
- add_manual(full_path)
+ def add_directory(path: Path) -> None:
+ for full_path in filter(package_like, path.iterdir()):
+ add_archive(full_path)
- def add_manual(name: str) -> str:
- package = Package.load(name, self.repository.pacman, self.config.get('alpm', 'aur_url'))
- path = os.path.join(self.repository.paths.manual, package.base)
+ def add_manual(src: str) -> Path:
+ package = Package.load(src, self.repository.pacman, self.config.get("alpm", "aur_url"))
+ path = self.repository.paths.manual / package.base
Task.fetch(path, package.git_url)
return path
- def add_archive(src: str) -> None:
- dst = os.path.join(self.repository.paths.packages, os.path.basename(src))
+ def add_archive(src: Path) -> None:
+ dst = self.repository.paths.packages / src.name
shutil.move(src, dst)
- def process_dependencies(path: str) -> None:
+ def process_dependencies(path: Path) -> None:
if without_dependencies:
return
dependencies = Package.dependencies(path)
self.add(dependencies.difference(known_packages), without_dependencies)
- def process_single(name: str) -> None:
- if os.path.isdir(name):
- add_directory(name)
- elif os.path.isfile(name):
- add_archive(name)
+ def process_single(src: str) -> None:
+ maybe_path = Path(src)
+ if maybe_path.is_dir():
+ add_directory(maybe_path)
+ elif maybe_path.is_file():
+ add_archive(maybe_path)
else:
- path = add_manual(name)
+ path = add_manual(src)
process_dependencies(path)
for name in names:
process_single(name)
def clean(self, no_build: bool, no_cache: bool, no_chroot: bool, no_manual: bool, no_packages: bool) -> None:
- '''
+ """
run all clean methods. Warning: some functions might not be available under non-root
:param no_build: do not clear directory with package sources
:param no_cache: do not clear directory with package caches
:param no_chroot: do not clear build chroot
:param no_manual: do not clear directory with manually added packages
:param no_packages: do not clear directory with built packages
- '''
+ """
if not no_build:
self.repository.clear_build()
if not no_cache:
@@ -155,35 +155,35 @@ class Application:
self.repository.clear_packages()
def remove(self, names: Iterable[str]) -> None:
- '''
+ """
remove packages from repository
:param names: list of packages (either base or name) to remove
- '''
+ """
self.repository.process_remove(names)
self._finalize()
def report(self, target: Optional[Iterable[str]] = None) -> None:
- '''
+ """
generate report
:param target: list of targets to run (e.g. html)
- '''
+ """
targets = target or None
self.repository.process_report(targets)
def sync(self, target: Optional[Iterable[str]] = None) -> None:
- '''
+ """
sync to remote server
:param target: list of targets to run (e.g. s3)
- '''
+ """
targets = target or None
self.repository.process_sync(targets)
def update(self, updates: Iterable[Package]) -> None:
- '''
+ """
run package updates
:param updates: list of packages to update
- '''
- def process_update(paths: Iterable[str]) -> None:
+ """
+ def process_update(paths: Iterable[Path]) -> None:
self.repository.process_update(paths)
self._finalize()
@@ -192,9 +192,8 @@ class Application:
process_update(packages)
# process manual packages
- tree = Tree()
- tree.load(updates)
+ tree = Tree.load(updates)
for num, level in enumerate(tree.levels()):
- self.logger.info(f'processing level #{num} {[package.base for package in level]}')
+ self.logger.info(f"processing level #{num} {[package.base for package in level]}")
packages = self.repository.process_build(level)
process_update(packages)
diff --git a/src/ahriman/application/handlers/__init__.py b/src/ahriman/application/handlers/__init__.py
index 5f1ea37d..7422bb01 100644
--- a/src/ahriman/application/handlers/__init__.py
+++ b/src/ahriman/application/handlers/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/application/handlers/add.py b/src/ahriman/application/handlers/add.py
index e8c9aeca..04f412a6 100644
--- a/src/ahriman/application/handlers/add.py
+++ b/src/ahriman/application/handlers/add.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
class Add(Handler):
- '''
+ """
add packages handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Application(architecture, config).add(args.package, args.without_dependencies)
diff --git a/src/ahriman/application/handlers/clean.py b/src/ahriman/application/handlers/clean.py
index 7e47aa4f..4ce130e1 100644
--- a/src/ahriman/application/handlers/clean.py
+++ b/src/ahriman/application/handlers/clean.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,17 +27,17 @@ from ahriman.core.configuration import Configuration
class Clean(Handler):
- '''
+ """
clean caches handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Application(architecture, config).clean(args.no_build, args.no_cache, args.no_chroot,
args.no_manual, args.no_packages)
diff --git a/src/ahriman/application/handlers/dump.py b/src/ahriman/application/handlers/dump.py
index 237baa6d..5791158d 100644
--- a/src/ahriman/application/handlers/dump.py
+++ b/src/ahriman/application/handlers/dump.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -26,21 +26,21 @@ from ahriman.core.configuration import Configuration
class Dump(Handler):
- '''
+ """
dump config handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
config_dump = config.dump(architecture)
for section, values in sorted(config_dump.items()):
- print(f'[{section}]')
+ print(f"[{section}]")
for key, value in sorted(values.items()):
- print(f'{key} = {value}')
+ print(f"{key} = {value}")
print()
diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py
index c1830c65..05332010 100644
--- a/src/ahriman/application/handlers/handler.py
+++ b/src/ahriman/application/handlers/handler.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -21,8 +21,8 @@ from __future__ import annotations
import argparse
import logging
-from multiprocessing import Pool
+from multiprocessing import Pool
from typing import Type
from ahriman.application.lock import Lock
@@ -30,34 +30,34 @@ from ahriman.core.configuration import Configuration
class Handler:
- '''
+ """
base handler class for command callbacks
- '''
+ """
@classmethod
def _call(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> bool:
- '''
+ """
additional function to wrap all calls for multiprocessing library
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
:return: True on success, False otherwise
- '''
+ """
try:
with Lock(args, architecture, config):
cls.run(args, architecture, config)
return True
except Exception:
- logging.getLogger('root').exception('process exception', exc_info=True)
+ logging.getLogger("root").exception("process exception")
return False
@classmethod
def execute(cls: Type[Handler], args: argparse.Namespace) -> int:
- '''
+ """
execute function for all aru
:param args: command line args
:return: 0 on success, 1 otherwise
- '''
+ """
configuration = Configuration.from_path(args.config, not args.no_log)
with Pool(len(args.architecture)) as pool:
result = pool.starmap(
@@ -66,10 +66,10 @@ class Handler:
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
raise NotImplementedError
diff --git a/src/ahriman/application/handlers/rebuild.py b/src/ahriman/application/handlers/rebuild.py
index 6c2616ae..fd5f0656 100644
--- a/src/ahriman/application/handlers/rebuild.py
+++ b/src/ahriman/application/handlers/rebuild.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,18 +27,18 @@ from ahriman.core.configuration import Configuration
class Rebuild(Handler):
- '''
+ """
make world handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
application = Application(architecture, config)
packages = application.repository.packages()
application.update(packages)
diff --git a/src/ahriman/application/handlers/remove.py b/src/ahriman/application/handlers/remove.py
index 5a7ac5b4..4ef9aa24 100644
--- a/src/ahriman/application/handlers/remove.py
+++ b/src/ahriman/application/handlers/remove.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
class Remove(Handler):
- '''
+ """
remove packages handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Application(architecture, config).remove(args.package)
diff --git a/src/ahriman/application/handlers/report.py b/src/ahriman/application/handlers/report.py
index 7c24417b..36625fe0 100644
--- a/src/ahriman/application/handlers/report.py
+++ b/src/ahriman/application/handlers/report.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
class Report(Handler):
- '''
+ """
generate report handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Application(architecture, config).report(args.target)
diff --git a/src/ahriman/application/handlers/status.py b/src/ahriman/application/handlers/status.py
index 25ed4634..0db0a561 100644
--- a/src/ahriman/application/handlers/status.py
+++ b/src/ahriman/application/handlers/status.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -29,18 +29,18 @@ from ahriman.models.package import Package
class Status(Handler):
- '''
+ """
package status handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
application = Application(architecture, config)
if args.ahriman:
ahriman = application.repository.reporter.get_self()
@@ -54,5 +54,5 @@ class Status(Handler):
packages = application.repository.reporter.get(None)
for package, package_status in sorted(packages, key=lambda item: item[0].base):
print(package.pretty_print())
- print(f'\t{package.version}')
- print(f'\t{package_status.pretty_print()}')
+ print(f"\t{package.version}")
+ print(f"\t{package_status.pretty_print()}")
diff --git a/src/ahriman/application/handlers/sync.py b/src/ahriman/application/handlers/sync.py
index 931776cf..f853de72 100644
--- a/src/ahriman/application/handlers/sync.py
+++ b/src/ahriman/application/handlers/sync.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,16 +27,16 @@ from ahriman.core.configuration import Configuration
class Sync(Handler):
- '''
+ """
remove sync handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Application(architecture, config).sync(args.target)
diff --git a/src/ahriman/application/handlers/update.py b/src/ahriman/application/handlers/update.py
index fa56d89c..f1d6b1f0 100644
--- a/src/ahriman/application/handlers/update.py
+++ b/src/ahriman/application/handlers/update.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,18 +27,18 @@ from ahriman.core.configuration import Configuration
class Update(Handler):
- '''
+ """
package update handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
# typing workaround
def log_fn(line: str) -> None:
return print(line) if args.dry_run else application.logger.info(line)
diff --git a/src/ahriman/application/handlers/web.py b/src/ahriman/application/handlers/web.py
index 5cb438c1..ef94098a 100644
--- a/src/ahriman/application/handlers/web.py
+++ b/src/ahriman/application/handlers/web.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -26,18 +26,18 @@ from ahriman.core.configuration import Configuration
class Web(Handler):
- '''
+ """
web server handler
- '''
+ """
@classmethod
def run(cls: Type[Handler], args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
callback for command line
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
from ahriman.web.web import run_server, setup_service
application = setup_service(architecture, config)
- run_server(application, architecture)
+ run_server(application)
diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py
index 61e1d294..0ab516a3 100644
--- a/src/ahriman/application/lock.py
+++ b/src/ahriman/application/lock.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -22,41 +22,42 @@ from __future__ import annotations
import argparse
import os
+from pathlib import Path
from types import TracebackType
from typing import Literal, Optional, Type
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRun, UnsafeRun
-from ahriman.core.watcher.client import Client
+from ahriman.core.status.client import Client
from ahriman.models.build_status import BuildStatusEnum
class Lock:
- '''
+ """
wrapper for application lock file
:ivar force: remove lock file on start if any
:ivar path: path to lock file if any
:ivar reporter: build status reporter instance
:ivar root: repository root (i.e. ahriman home)
:ivar unsafe: skip user check
- '''
+ """
def __init__(self, args: argparse.Namespace, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param args: command line args
:param architecture: repository architecture
:param config: configuration instance
- '''
- self.path = f'{args.lock}_{architecture}' if args.lock is not None else None
+ """
+ self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
self.force = args.force
self.unsafe = args.unsafe
- self.root = config.get('repository', 'root')
+ self.root = Path(config.get("repository", "root"))
self.reporter = Client() if args.no_report else Client.load(architecture, config)
def __enter__(self) -> Lock:
- '''
+ """
default workflow is the following:
check user UID
@@ -64,62 +65,52 @@ class Lock:
check if there is lock file
create lock file
report to web if enabled
- '''
+ """
self.check_user()
- if self.force:
- self.remove()
- self.check()
self.create()
self.reporter.update_self(BuildStatusEnum.Building)
return self
def __exit__(self, exc_type: Optional[Type[Exception]], exc_val: Optional[Exception],
exc_tb: TracebackType) -> Literal[False]:
- '''
+ """
remove lock file when done
:param exc_type: exception type name if any
:param exc_val: exception raised if any
:param exc_tb: exception traceback if any
:return: always False (do not suppress any exception)
- '''
+ """
self.remove()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
self.reporter.update_self(status)
return False
- def check(self) -> None:
- '''
- check if lock file exists, raise exception if it does
- '''
- if self.path is None:
- return
- if os.path.exists(self.path):
- raise DuplicateRun()
-
def check_user(self) -> None:
- '''
+ """
check if current user is actually owner of ahriman root
- '''
+ """
if self.unsafe:
return
current_uid = os.getuid()
- root_uid = os.stat(self.root).st_uid
+ root_uid = self.root.stat().st_uid
if current_uid != root_uid:
raise UnsafeRun(current_uid, root_uid)
def create(self) -> None:
- '''
+ """
create lock file
- '''
+ """
if self.path is None:
return
- open(self.path, 'w').close()
+ try:
+ self.path.touch(exist_ok=self.force)
+ except FileExistsError:
+ raise DuplicateRun()
def remove(self) -> None:
- '''
+ """
remove lock file
- '''
+ """
if self.path is None:
return
- if os.path.exists(self.path):
- os.remove(self.path)
+ self.path.unlink(missing_ok=True)
diff --git a/src/ahriman/core/__init__.py b/src/ahriman/core/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/__init__.py
+++ b/src/ahriman/core/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/alpm/__init__.py b/src/ahriman/core/alpm/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/alpm/__init__.py
+++ b/src/ahriman/core/alpm/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/alpm/pacman.py b/src/ahriman/core/alpm/pacman.py
index f5eb0cf2..619764ca 100644
--- a/src/ahriman/core/alpm/pacman.py
+++ b/src/ahriman/core/alpm/pacman.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -24,27 +24,27 @@ from ahriman.core.configuration import Configuration
class Pacman:
- '''
+ """
alpm wrapper
:ivar handle: pyalpm root `Handle`
- '''
+ """
def __init__(self, config: Configuration) -> None:
- '''
+ """
default constructor
:param config: configuration instance
- '''
- root = config.get('alpm', 'root')
- pacman_root = config.get('alpm', 'database')
- self.handle = Handle(root, pacman_root)
- for repository in config.getlist('alpm', 'repositories'):
+ """
+ root = config.get("alpm", "root")
+ pacman_root = config.getpath("alpm", "database")
+ self.handle = Handle(root, str(pacman_root))
+ for repository in config.getlist("alpm", "repositories"):
self.handle.register_syncdb(repository, 0) # 0 is pgp_level
def all_packages(self) -> List[str]:
- '''
+ """
get list of packages known for alpm
:return: list of package names
- '''
+ """
result: Set[str] = set()
for database in self.handle.get_syncdbs():
result.update({package.name for package in database.pkgcache})
diff --git a/src/ahriman/core/alpm/repo.py b/src/ahriman/core/alpm/repo.py
index e55350f8..6c83e481 100644
--- a/src/ahriman/core/alpm/repo.py
+++ b/src/ahriman/core/alpm/repo.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -18,8 +18,8 @@
# along with this program. If not, see .
#
import logging
-import os
+from pathlib import Path
from typing import List
from ahriman.core.exceptions import BuildFailed
@@ -28,56 +28,59 @@ from ahriman.models.repository_paths import RepositoryPaths
class Repo:
- '''
+ """
repo-add and repo-remove wrapper
:ivar logger: class logger
:ivar name: repository name
:ivar paths: repository paths instance
:ivar sign_args: additional args which have to be used to sign repository archive
- '''
+ """
+
+ _check_output = check_output
def __init__(self, name: str, paths: RepositoryPaths, sign_args: List[str]) -> None:
- '''
+ """
default constructor
:param name: repository name
:param paths: repository paths instance
:param sign_args: additional args which have to be used to sign repository archive
- '''
- self.logger = logging.getLogger('build_details')
+ """
+ self.logger = logging.getLogger("build_details")
self.name = name
self.paths = paths
self.sign_args = sign_args
@property
- def repo_path(self) -> str:
- '''
+ def repo_path(self) -> Path:
+ """
:return: path to repository database
- '''
- return os.path.join(self.paths.repository, f'{self.name}.db.tar.gz')
+ """
+ return self.paths.repository / f"{self.name}.db.tar.gz"
- def add(self, path: str) -> None:
- '''
+ def add(self, path: Path) -> None:
+ """
add new package to repository
:param path: path to archive to add
- '''
- check_output(
- 'repo-add', *self.sign_args, '-R', self.repo_path, path,
- exception=BuildFailed(path),
+ """
+ Repo._check_output(
+ "repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
+ exception=BuildFailed(path.name),
cwd=self.paths.repository,
logger=self.logger)
- def remove(self, package: str) -> None:
- '''
+ def remove(self, package: str, filename: Path) -> None:
+ """
remove package from repository
:param package: package name to remove
- '''
+ :param filename: package filename to remove
+ """
# remove package and signature (if any) from filesystem
- for fn in filter(lambda f: f.startswith(package), os.listdir(self.paths.repository)):
- full_path = os.path.join(self.paths.repository, fn)
- os.remove(full_path)
+ for full_path in self.paths.repository.glob(f"{filename}*"):
+ full_path.unlink()
+
# remove package from registry
- check_output(
- 'repo-remove', *self.sign_args, self.repo_path, package,
+ Repo._check_output(
+ "repo-remove", *self.sign_args, str(self.repo_path), package,
exception=BuildFailed(package),
cwd=self.paths.repository,
logger=self.logger)
diff --git a/src/ahriman/core/build_tools/__init__.py b/src/ahriman/core/build_tools/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/build_tools/__init__.py
+++ b/src/ahriman/core/build_tools/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/build_tools/task.py b/src/ahriman/core/build_tools/task.py
index 5fa85fc3..8a991ed8 100644
--- a/src/ahriman/core/build_tools/task.py
+++ b/src/ahriman/core/build_tools/task.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,10 +17,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-import os
import logging
import shutil
+from pathlib import Path
from typing import List, Optional
from ahriman.core.configuration import Configuration
@@ -31,94 +31,98 @@ from ahriman.models.repository_paths import RepositoryPaths
class Task:
- '''
+ """
base package build task
:ivar build_logger: logger for build process
:ivar logger: class logger
:ivar package: package definitions
:ivar paths: repository paths instance
- '''
+ """
+
+ _check_output = check_output
def __init__(self, package: Package, architecture: str, config: Configuration, paths: RepositoryPaths) -> None:
- '''
+ """
default constructor
:param package: package definitions
:param architecture: repository architecture
:param config: configuration instance
:param paths: repository paths instance
- '''
- self.logger = logging.getLogger('builder')
- self.build_logger = logging.getLogger('build_details')
+ """
+ self.logger = logging.getLogger("builder")
+ self.build_logger = logging.getLogger("build_details")
self.package = package
self.paths = paths
- section = config.get_section_name('build', architecture)
- self.archbuild_flags = config.getlist(section, 'archbuild_flags')
- self.build_command = config.get(section, 'build_command')
- self.makepkg_flags = config.getlist(section, 'makepkg_flags')
- self.makechrootpkg_flags = config.getlist(section, 'makechrootpkg_flags')
+ section = config.get_section_name("build", architecture)
+ self.archbuild_flags = config.getlist(section, "archbuild_flags")
+ self.build_command = config.get(section, "build_command")
+ self.makepkg_flags = config.getlist(section, "makepkg_flags")
+ self.makechrootpkg_flags = config.getlist(section, "makechrootpkg_flags")
@property
- def cache_path(self) -> str:
- '''
+ def cache_path(self) -> Path:
+ """
:return: path to cached packages
- '''
- return os.path.join(self.paths.cache, self.package.base)
+ """
+ return self.paths.cache / self.package.base
@property
- def git_path(self) -> str:
- '''
+ def git_path(self) -> Path:
+ """
:return: path to clone package from git
- '''
- return os.path.join(self.paths.sources, self.package.base)
+ """
+ return self.paths.sources / self.package.base
@staticmethod
- def fetch(local: str, remote: str, branch: str = 'master') -> None:
- '''
+ def fetch(local: Path, remote: str, branch: str = "master") -> None:
+ """
either clone repository or update it to origin/`branch`
:param local: local path to fetch
:param remote: remote target (from where to fetch)
:param branch: branch name to checkout, master by default
- '''
- logger = logging.getLogger('build_details')
+ """
+ logger = logging.getLogger("build_details")
# local directory exists and there is .git directory
- if os.path.isdir(os.path.join(local, '.git')):
- check_output('git', 'fetch', 'origin', branch, exception=None, cwd=local, logger=logger)
+ if (local / ".git").is_dir():
+ Task._check_output("git", "fetch", "origin", branch, exception=None, cwd=local, logger=logger)
else:
- check_output('git', 'clone', remote, local, exception=None, logger=logger)
+ Task._check_output("git", "clone", remote, str(local), exception=None, logger=logger)
# and now force reset to our branch
- check_output('git', 'reset', '--hard', f'origin/{branch}', exception=None, cwd=local, logger=logger)
+ Task._check_output("git", "checkout", "--force", branch, exception=None, cwd=local, logger=logger)
+ Task._check_output("git", "reset", "--hard", f"origin/{branch}", exception=None, cwd=local, logger=logger)
- def build(self) -> List[str]:
- '''
+ def build(self) -> List[Path]:
+ """
run package build
:return: paths of produced packages
- '''
- cmd = [self.build_command, '-r', self.paths.chroot]
+ """
+ cmd = [self.build_command, "-r", str(self.paths.chroot)]
cmd.extend(self.archbuild_flags)
- cmd.extend(['--'] + self.makechrootpkg_flags)
- cmd.extend(['--'] + self.makepkg_flags)
- self.logger.info(f'using {cmd} for {self.package.base}')
+ cmd.extend(["--"] + self.makechrootpkg_flags)
+ cmd.extend(["--"] + self.makepkg_flags)
+ self.logger.info(f"using {cmd} for {self.package.base}")
- check_output(
+ Task._check_output(
*cmd,
exception=BuildFailed(self.package.base),
cwd=self.git_path,
logger=self.build_logger)
# well it is not actually correct, but we can deal with it
- return check_output('makepkg', '--packagelist',
- exception=BuildFailed(self.package.base),
- cwd=self.git_path,
- logger=self.build_logger).splitlines()
+ packages = Task._check_output("makepkg", "--packagelist",
+ exception=BuildFailed(self.package.base),
+ cwd=self.git_path,
+ logger=self.build_logger).splitlines()
+ return [Path(package) for package in packages]
- def init(self, path: Optional[str] = None) -> None:
- '''
+ def init(self, path: Optional[Path] = None) -> None:
+ """
fetch package from git
:param path: optional local path to fetch. If not set default path will be used
- '''
+ """
git_path = path or self.git_path
- if os.path.isdir(self.cache_path):
+ if self.cache_path.is_dir():
# no need to clone whole repository, just copy from cache first
shutil.copytree(self.cache_path, git_path)
return Task.fetch(git_path, self.package.git_url)
diff --git a/src/ahriman/core/configuration.py b/src/ahriman/core/configuration.py
index 2eb58d3b..d9b93929 100644
--- a/src/ahriman/core/configuration.py
+++ b/src/ahriman/core/configuration.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -21,61 +21,61 @@ from __future__ import annotations
import configparser
import logging
-import os
from logging.config import fileConfig
+from pathlib import Path
from typing import Dict, List, Optional, Type
class Configuration(configparser.RawConfigParser):
- '''
+ """
extension for built-in configuration parser
:ivar path: path to root configuration file
:cvar ARCHITECTURE_SPECIFIC_SECTIONS: known sections which can be architecture specific (required by dump)
:cvar DEFAULT_LOG_FORMAT: default log format (in case of fallback)
:cvar DEFAULT_LOG_LEVEL: default log level (in case of fallback)
:cvar STATIC_SECTIONS: known sections which are not architecture specific (required by dump)
- '''
+ """
- DEFAULT_LOG_FORMAT = '[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s'
+ DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG
- STATIC_SECTIONS = ['alpm', 'report', 'repository', 'settings', 'upload']
- ARCHITECTURE_SPECIFIC_SECTIONS = ['build', 'html', 'rsync', 's3', 'sign', 'web']
+ STATIC_SECTIONS = ["alpm", "report", "repository", "settings", "upload"]
+ ARCHITECTURE_SPECIFIC_SECTIONS = ["build", "html", "rsync", "s3", "sign", "web"]
def __init__(self) -> None:
- '''
+ """
default constructor. In the most cases must not be called directly
- '''
+ """
configparser.RawConfigParser.__init__(self, allow_no_value=True)
- self.path: Optional[str] = None
+ self.path: Optional[Path] = None
@property
- def include(self) -> str:
- '''
+ def include(self) -> Path:
+ """
:return: path to directory with configuration includes
- '''
- return self.get('settings', 'include')
+ """
+ return self.getpath("settings", "include")
@classmethod
- def from_path(cls: Type[Configuration], path: str, logfile: bool) -> Configuration:
- '''
+ def from_path(cls: Type[Configuration], path: Path, logfile: bool) -> Configuration:
+ """
constructor with full object initialization
:param path: path to root configuration file
:param logfile: use log file to output messages
:return: configuration instance
- '''
+ """
config = cls()
config.load(path)
config.load_logging(logfile)
return config
def dump(self, architecture: str) -> Dict[str, Dict[str, str]]:
- '''
+ """
dump configuration to dictionary
:param architecture: repository architecture
:return: configuration dump for specific architecture
- '''
+ """
result: Dict[str, Dict[str, str]] = {}
for section in Configuration.STATIC_SECTIONS:
if not self.has_section(section):
@@ -90,57 +90,70 @@ class Configuration(configparser.RawConfigParser):
return result
def getlist(self, section: str, key: str) -> List[str]:
- '''
+ """
get space separated string list option
:param section: section name
:param key: key name
:return: list of string if option is set, empty list otherwise
- '''
+ """
raw = self.get(section, key, fallback=None)
if not raw: # empty string or none
return []
return raw.split()
+ def getpath(self, section: str, key: str) -> Path:
+ """
+ helper to generate absolute configuration path for relative settings value
+ :param section: section name
+ :param key: key name
+ :return: absolute path according to current path configuration
+ """
+ value = Path(self.get(section, key))
+ if self.path is None or value.is_absolute():
+ return value
+ return self.path.parent / value
+
def get_section_name(self, prefix: str, suffix: str) -> str:
- '''
+ """
check if there is `prefix`_`suffix` section and return it on success. Return `prefix` otherwise
:param prefix: section name prefix
:param suffix: section name suffix (e.g. architecture name)
:return: found section name
- '''
- probe = f'{prefix}_{suffix}'
+ """
+ probe = f"{prefix}_{suffix}"
return probe if self.has_section(probe) else prefix
- def load(self, path: str) -> None:
- '''
+ def load(self, path: Path) -> None:
+ """
fully load configuration
:param path: path to root configuration file
- '''
+ """
self.path = path
self.read(self.path)
self.load_includes()
def load_includes(self) -> None:
- '''
+ """
load configuration includes
- '''
+ """
try:
- for conf in filter(lambda p: p.endswith('.ini'), sorted(os.listdir(self.include))):
- self.read(os.path.join(self.include, conf))
+ for path in sorted(self.include.glob("*.ini")):
+ self.read(path)
except (FileNotFoundError, configparser.NoOptionError):
pass
def load_logging(self, logfile: bool) -> None:
- '''
+ """
setup logging settings from configuration
:param logfile: use log file to output messages
- '''
+ """
def file_logger() -> None:
try:
- fileConfig(self.get('settings', 'logging'))
- except PermissionError:
+ config_path = self.getpath("settings", "logging")
+ fileConfig(config_path)
+ except (FileNotFoundError, PermissionError):
console_logger()
- logging.error('could not create logfile, fallback to stderr', exc_info=True)
+ logging.exception("could not create logfile, fallback to stderr")
def console_logger() -> None:
logging.basicConfig(filename=None, format=Configuration.DEFAULT_LOG_FORMAT,
diff --git a/src/ahriman/core/exceptions.py b/src/ahriman/core/exceptions.py
index c20c4d60..2050b75d 100644
--- a/src/ahriman/core/exceptions.py
+++ b/src/ahriman/core/exceptions.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -21,103 +21,112 @@ from typing import Any
class BuildFailed(Exception):
- '''
+ """
base exception for failed builds
- '''
+ """
def __init__(self, package: str) -> None:
- '''
+ """
default constructor
:param package: package base raised exception
- '''
- Exception.__init__(self, f'Package {package} build failed, check logs for details')
+ """
+ Exception.__init__(self, f"Package {package} build failed, check logs for details")
class DuplicateRun(Exception):
- '''
+ """
exception which will be raised if there is another application instance
- '''
+ """
def __init__(self) -> None:
- '''
+ """
default constructor
- '''
- Exception.__init__(self, 'Another application instance is run')
+ """
+ Exception.__init__(self, "Another application instance is run")
class InitializeException(Exception):
- '''
+ """
base service initialization exception
- '''
+ """
def __init__(self) -> None:
- '''
+ """
default constructor
- '''
- Exception.__init__(self, 'Could not load service')
+ """
+ Exception.__init__(self, "Could not load service")
class InvalidOption(Exception):
- '''
+ """
exception which will be raised on configuration errors
- '''
+ """
def __init__(self, value: Any) -> None:
- '''
+ """
default constructor
:param value: option value
- '''
- Exception.__init__(self, f'Invalid or unknown option value `{value}`')
+ """
+ Exception.__init__(self, f"Invalid or unknown option value `{value}`")
class InvalidPackageInfo(Exception):
- '''
+ """
exception which will be raised on package load errors
- '''
+ """
def __init__(self, details: Any) -> None:
- '''
+ """
default constructor
:param details: error details
- '''
- Exception.__init__(self, f'There are errors during reading package information: `{details}`')
+ """
+ Exception.__init__(self, f"There are errors during reading package information: `{details}`")
class ReportFailed(Exception):
- '''
+ """
report generation exception
- '''
+ """
def __init__(self) -> None:
- '''
+ """
default constructor
- '''
- Exception.__init__(self, 'Report failed')
+ """
+ Exception.__init__(self, "Report failed")
class SyncFailed(Exception):
- '''
+ """
remote synchronization exception
- '''
+ """
def __init__(self) -> None:
- '''
+ """
default constructor
- '''
- Exception.__init__(self, 'Sync failed')
+ """
+ Exception.__init__(self, "Sync failed")
+
+
+class UnknownPackage(Exception):
+ """
+ exception for status watcher which will be thrown on unknown package
+ """
+
+ def __init__(self, base: str) -> None:
+ Exception.__init__(self, f"Package base {base} is unknown")
class UnsafeRun(Exception):
- '''
+ """
exception which will be raised in case if user is not owner of repository
- '''
+ """
def __init__(self, current_uid: int, root_uid: int) -> None:
- '''
+ """
default constructor
- '''
+ """
Exception.__init__(
self,
- f'''Current UID {current_uid} differs from root owner {root_uid}.
+ f"""Current UID {current_uid} differs from root owner {root_uid}.
Note that for the most actions it is unsafe to run application as different user.
-If you are 100% sure that it must be there try --unsafe option''')
+If you are 100% sure that it must be there try --unsafe option""")
diff --git a/src/ahriman/core/report/__init__.py b/src/ahriman/core/report/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/report/__init__.py
+++ b/src/ahriman/core/report/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/report/html.py b/src/ahriman/core/report/html.py
index 35416146..3bff8567 100644
--- a/src/ahriman/core/report/html.py
+++ b/src/ahriman/core/report/html.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -18,7 +18,6 @@
# along with this program. If not, see .
#
import jinja2
-import os
from typing import Callable, Dict, Iterable
@@ -30,7 +29,7 @@ from ahriman.models.sign_settings import SignSettings
class HTML(Report):
- '''
+ """
html report generator
It uses jinja2 templates for report generation, the following variables are allowed:
@@ -50,50 +49,49 @@ class HTML(Report):
:ivar report_path: output path to html report
:ivar sign_targets: targets to sign enabled in configuration
:ivar tempate_path: path to directory with jinja templates
- '''
+ """
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Report.__init__(self, architecture, config)
- section = config.get_section_name('html', architecture)
- self.report_path = config.get(section, 'path')
- self.link_path = config.get(section, 'link_path')
- self.template_path = config.get(section, 'template_path')
+ section = config.get_section_name("html", architecture)
+ self.report_path = config.getpath(section, "path")
+ self.link_path = config.get(section, "link_path")
+ self.template_path = config.getpath(section, "template_path")
# base template vars
- self.homepage = config.get(section, 'homepage', fallback=None)
- self.name = config.get('repository', 'name')
+ self.homepage = config.get(section, "homepage", fallback=None)
+ self.name = config.get("repository", "name")
- sign_section = config.get_section_name('sign', architecture)
- self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, 'target')]
- self.pgp_key = config.get(sign_section, 'key') if self.sign_targets else None
+ sign_section = config.get_section_name("sign", architecture)
+ self.sign_targets = [SignSettings.from_option(opt) for opt in config.getlist(sign_section, "target")]
+ self.pgp_key = config.get(sign_section, "key") if self.sign_targets else None
def generate(self, packages: Iterable[Package]) -> None:
- '''
+ """
generate report for the specified packages
:param packages: list of packages to generate report
- '''
+ """
# idea comes from https://stackoverflow.com/a/38642558
- templates_dir, template_name = os.path.split(self.template_path)
- loader = jinja2.FileSystemLoader(searchpath=templates_dir)
+ loader = jinja2.FileSystemLoader(searchpath=self.template_path.parent)
environment = jinja2.Environment(loader=loader)
- template = environment.get_template(template_name)
+ template = environment.get_template(self.template_path.name)
content = [
{
- 'archive_size': pretty_size(properties.archive_size),
- 'build_date': pretty_datetime(properties.build_date),
- 'filename': properties.filename,
- 'installed_size': pretty_size(properties.installed_size),
- 'name': package,
- 'version': base.version
+ "archive_size": pretty_size(properties.archive_size),
+ "build_date": pretty_datetime(properties.build_date),
+ "filename": properties.filename,
+ "installed_size": pretty_size(properties.installed_size),
+ "name": package,
+ "version": base.version
} for base in packages for package, properties in base.packages.items()
]
- comparator: Callable[[Dict[str, str]], str] = lambda item: item['filename']
+ comparator: Callable[[Dict[str, str]], str] = lambda item: item["filename"]
html = template.render(
homepage=self.homepage,
@@ -104,5 +102,4 @@ class HTML(Report):
pgp_key=self.pgp_key,
repository=self.name)
- with open(self.report_path, 'w') as out:
- out.write(html)
+ self.report_path.write_text(html)
diff --git a/src/ahriman/core/report/report.py b/src/ahriman/core/report/report.py
index 81df62d5..e85387b0 100644
--- a/src/ahriman/core/report/report.py
+++ b/src/ahriman/core/report/report.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -28,32 +28,32 @@ from ahriman.models.report_settings import ReportSettings
class Report:
- '''
+ """
base report generator
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar logger: class logger
- '''
+ """
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
- self.logger = logging.getLogger('builder')
+ """
+ self.logger = logging.getLogger("builder")
self.architecture = architecture
self.config = config
@staticmethod
def run(architecture: str, config: Configuration, target: str, packages: Iterable[Package]) -> None:
- '''
+ """
run report generation
:param architecture: repository architecture
:param config: configuration instance
:param target: target to generate report (e.g. html)
:param packages: list of packages to generate report
- '''
+ """
provider = ReportSettings.from_option(target)
if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML
@@ -64,11 +64,11 @@ class Report:
try:
report.generate(packages)
except Exception:
- report.logger.exception('report generation failed', exc_info=True)
+ report.logger.exception(f"report generation failed for target {provider.name}")
raise ReportFailed()
def generate(self, packages: Iterable[Package]) -> None:
- '''
+ """
generate report for the specified packages
:param packages: list of packages to generate report
- '''
+ """
diff --git a/src/ahriman/core/repository/__init__.py b/src/ahriman/core/repository/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/repository/__init__.py
+++ b/src/ahriman/core/repository/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/repository/cleaner.py b/src/ahriman/core/repository/cleaner.py
index 363f8c3e..62665a73 100644
--- a/src/ahriman/core/repository/cleaner.py
+++ b/src/ahriman/core/repository/cleaner.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,62 +17,62 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-import os
import shutil
+from pathlib import Path
from typing import List
from ahriman.core.repository.properties import Properties
class Cleaner(Properties):
- '''
+ """
trait to clean common repository objects
- '''
+ """
- def packages_built(self) -> List[str]:
- '''
+ def packages_built(self) -> List[Path]:
+ """
get list of files in built packages directory
:return: list of filenames from the directory
- '''
+ """
raise NotImplementedError
def clear_build(self) -> None:
- '''
+ """
clear sources directory
- '''
- self.logger.info('clear package sources directory')
- for package in os.listdir(self.paths.sources):
- shutil.rmtree(os.path.join(self.paths.sources, package))
+ """
+ self.logger.info("clear package sources directory")
+ for package in self.paths.sources.iterdir():
+ shutil.rmtree(package)
def clear_cache(self) -> None:
- '''
+ """
clear cache directory
- '''
- self.logger.info('clear packages sources cache directory')
- for package in os.listdir(self.paths.cache):
- shutil.rmtree(os.path.join(self.paths.cache, package))
+ """
+ self.logger.info("clear packages sources cache directory")
+ for package in self.paths.cache.iterdir():
+ shutil.rmtree(package)
def clear_chroot(self) -> None:
- '''
+ """
clear cache directory. Warning: this method is architecture independent and will clear every chroot
- '''
- self.logger.info('clear build chroot directory')
- for chroot in os.listdir(self.paths.chroot):
- shutil.rmtree(os.path.join(self.paths.chroot, chroot))
+ """
+ self.logger.info("clear build chroot directory")
+ for chroot in self.paths.chroot.iterdir():
+ shutil.rmtree(chroot)
def clear_manual(self) -> None:
- '''
+ """
clear directory with manual package updates
- '''
- self.logger.info('clear manual packages')
- for package in os.listdir(self.paths.manual):
- shutil.rmtree(os.path.join(self.paths.manual, package))
+ """
+ self.logger.info("clear manual packages")
+ for package in self.paths.manual.iterdir():
+ shutil.rmtree(package)
def clear_packages(self) -> None:
- '''
+ """
clear directory with built packages (NOT repository itself)
- '''
- self.logger.info('clear built packages directory')
+ """
+ self.logger.info("clear built packages directory")
for package in self.packages_built():
- os.remove(package)
+ package.unlink()
diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py
index 60c14aa7..10c17d14 100644
--- a/src/ahriman/core/repository/executor.py
+++ b/src/ahriman/core/repository/executor.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,9 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-import os
import shutil
+from pathlib import Path
from typing import Dict, Iterable, List, Optional
from ahriman.core.build_tools.task import Task
@@ -30,113 +30,123 @@ from ahriman.models.package import Package
class Executor(Cleaner):
- '''
+ """
trait for common repository update processes
- '''
+ """
def packages(self) -> List[Package]:
- '''
+ """
generate list of repository packages
:return: list of packages properties
- '''
+ """
raise NotImplementedError
- def process_build(self, updates: Iterable[Package]) -> List[str]:
- '''
+ def process_build(self, updates: Iterable[Package]) -> List[Path]:
+ """
build packages
:param updates: list of packages properties to build
:return: `packages_built`
- '''
+ """
def build_single(package: Package) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.architecture, self.config, self.paths)
task.init()
built = task.build()
for src in built:
- dst = os.path.join(self.paths.packages, os.path.basename(src))
+ dst = self.paths.packages / src.name
shutil.move(src, dst)
- for package in updates:
+ for single in updates:
try:
- build_single(package)
+ build_single(single)
except Exception:
- self.reporter.set_failed(package.base)
- self.logger.exception(f'{package.base} ({self.architecture}) build exception', exc_info=True)
- continue
+ self.reporter.set_failed(single.base)
+ self.logger.exception(f"{single.base} ({self.architecture}) build exception")
self.clear_build()
return self.packages_built()
- def process_remove(self, packages: Iterable[str]) -> str:
- '''
+ def process_remove(self, packages: Iterable[str]) -> Path:
+ """
remove packages from list
- :param packages: list of package names or bases to rmeove
+ :param packages: list of package names or bases to remove
:return: path to repository database
- '''
- def remove_single(package: str) -> None:
+ """
+ def remove_single(package: str, fn: Path) -> None:
try:
- self.repo.remove(package)
+ self.repo.remove(package, fn)
except Exception:
- self.logger.exception(f'could not remove {package}', exc_info=True)
+ self.logger.exception(f"could not remove {package}")
requested = set(packages)
for local in self.packages():
- if local.base in packages:
- to_remove = set(local.packages.keys())
+ if local.base in packages or all(package in requested for package in local.packages):
+ to_remove = {
+ package: Path(properties.filename)
+ for package, properties in local.packages.items()
+ if properties.filename is not None
+ }
self.reporter.remove(local.base) # we only update status page in case of base removal
elif requested.intersection(local.packages.keys()):
- to_remove = requested.intersection(local.packages.keys())
+ to_remove = {
+ package: Path(properties.filename)
+ for package, properties in local.packages.items()
+ if package in requested and properties.filename is not None
+ }
else:
- to_remove = set()
- for package in to_remove:
- remove_single(package)
+ to_remove = dict()
+ for package, filename in to_remove.items():
+ remove_single(package, filename)
return self.repo.repo_path
def process_report(self, targets: Optional[Iterable[str]]) -> None:
- '''
+ """
generate reports
:param targets: list of targets to generate reports. Configuration option will be used if it is not set
- '''
+ """
if targets is None:
- targets = self.config.getlist('report', 'target')
+ targets = self.config.getlist("report", "target")
for target in targets:
Report.run(self.architecture, self.config, target, self.packages())
def process_sync(self, targets: Optional[Iterable[str]]) -> None:
- '''
+ """
process synchronization to remote servers
:param targets: list of targets to sync. Configuration option will be used if it is not set
- '''
+ """
if targets is None:
- targets = self.config.getlist('upload', 'target')
+ targets = self.config.getlist("upload", "target")
for target in targets:
Uploader.run(self.architecture, self.config, target, self.paths.repository)
- def process_update(self, packages: Iterable[str]) -> str:
- '''
+ def process_update(self, packages: Iterable[Path]) -> Path:
+ """
sign packages, add them to repository and update repository database
:param packages: list of filenames to run
:return: path to repository database
- '''
+ """
def update_single(fn: Optional[str], base: str) -> None:
if fn is None:
- self.logger.warning(f'received empty package name for base {base}')
+ self.logger.warning(f"received empty package name for base {base}")
return # suppress type checking, it never can be none actually
# in theory it might be NOT packages directory, but we suppose it is
- full_path = os.path.join(self.paths.packages, fn)
+ full_path = self.paths.packages / fn
files = self.sign.sign_package(full_path, base)
for src in files:
- dst = os.path.join(self.paths.repository, os.path.basename(src))
+ dst = self.paths.repository / src.name
shutil.move(src, dst)
- package_path = os.path.join(self.paths.repository, fn)
+ package_path = self.paths.repository / fn
self.repo.add(package_path)
# we are iterating over bases, not single packages
updates: Dict[str, Package] = {}
- for fn in packages:
- local = Package.load(fn, self.pacman, self.aur_url)
- updates.setdefault(local.base, local).packages.update(local.packages)
+ for filename in packages:
+ try:
+ local = Package.load(filename, self.pacman, self.aur_url)
+ updates.setdefault(local.base, local).packages.update(local.packages)
+ except Exception:
+ self.logger.exception(f"could not load package from {filename}")
for local in updates.values():
try:
@@ -145,7 +155,7 @@ class Executor(Cleaner):
self.reporter.set_success(local)
except Exception:
self.reporter.set_failed(local.base)
- self.logger.exception(f'could not process {local.base}', exc_info=True)
+ self.logger.exception(f"could not process {local.base}")
self.clear_packages()
return self.repo.repo_path
diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py
index 9e991006..4db2f12f 100644
--- a/src/ahriman/core/repository/properties.py
+++ b/src/ahriman/core/repository/properties.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -23,12 +23,12 @@ from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.repo import Repo
from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
-from ahriman.core.watcher.client import Client
+from ahriman.core.status.client import Client
from ahriman.models.repository_paths import RepositoryPaths
class Properties:
- '''
+ """
repository internal objects holder
:ivar architecture: repository architecture
:ivar aur_url: base AUR url
@@ -40,17 +40,17 @@ class Properties:
:ivar repo: repo commands wrapper instance
:ivar reporter: build status reporter instance
:ivar sign: GPG wrapper instance
- '''
+ """
def __init__(self, architecture: str, config: Configuration) -> None:
- self.logger = logging.getLogger('builder')
+ self.logger = logging.getLogger("builder")
self.architecture = architecture
self.config = config
- self.aur_url = config.get('alpm', 'aur_url')
- self.name = config.get('repository', 'name')
+ self.aur_url = config.get("alpm", "aur_url")
+ self.name = config.get("repository", "name")
- self.paths = RepositoryPaths(config.get('repository', 'root'), architecture)
+ self.paths = RepositoryPaths(config.getpath("repository", "root"), architecture)
self.paths.create_tree()
self.pacman = Pacman(config)
diff --git a/src/ahriman/core/repository/repository.py b/src/ahriman/core/repository/repository.py
index c108b427..e6c876b0 100644
--- a/src/ahriman/core/repository/repository.py
+++ b/src/ahriman/core/repository/repository.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,8 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-import os
-
+from pathlib import Path
from typing import Dict, List
from ahriman.core.repository.executor import Executor
@@ -28,34 +27,30 @@ from ahriman.models.package import Package
class Repository(Executor, UpdateHandler):
- '''
+ """
base repository control class
- '''
+ """
def packages(self) -> List[Package]:
- '''
+ """
generate list of repository packages
:return: list of packages properties
- '''
+ """
result: Dict[str, Package] = {}
- for fn in os.listdir(self.paths.repository):
- if not package_like(fn):
+ for full_path in self.paths.repository.iterdir():
+ if not package_like(full_path):
continue
- full_path = os.path.join(self.paths.repository, fn)
try:
local = Package.load(full_path, self.pacman, self.aur_url)
result.setdefault(local.base, local).packages.update(local.packages)
except Exception:
- self.logger.exception(f'could not load package from {fn}', exc_info=True)
+ self.logger.exception(f"could not load package from {full_path}")
continue
return list(result.values())
- def packages_built(self) -> List[str]:
- '''
+ def packages_built(self) -> List[Path]:
+ """
get list of files in built packages directory
:return: list of filenames from the directory
- '''
- return [
- os.path.join(self.paths.packages, fn)
- for fn in os.listdir(self.paths.packages)
- ]
+ """
+ return list(self.paths.packages.iterdir())
diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py
index 451b32ea..7aa87459 100644
--- a/src/ahriman/core/repository/update_handler.py
+++ b/src/ahriman/core/repository/update_handler.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-import os
-
from typing import Iterable, List
from ahriman.core.repository.cleaner import Cleaner
@@ -26,28 +24,28 @@ from ahriman.models.package import Package
class UpdateHandler(Cleaner):
- '''
+ """
trait to get package update list
- '''
+ """
def packages(self) -> List[Package]:
- '''
+ """
generate list of repository packages
:return: list of packages properties
- '''
+ """
raise NotImplementedError
def updates_aur(self, filter_packages: Iterable[str], no_vcs: bool) -> List[Package]:
- '''
+ """
check AUR for updates
:param filter_packages: do not check every package just specified in the list
:param no_vcs: do not check VCS packages
:return: list of packages which are out-of-dated
- '''
+ """
result: List[Package] = []
- build_section = self.config.get_section_name('build', self.architecture)
- ignore_list = self.config.getlist(build_section, 'ignore_packages')
+ build_section = self.config.get_section_name("build", self.architecture)
+ ignore_list = self.config.getlist(build_section, "ignore_packages")
for local in self.packages():
if local.base in ignore_list:
@@ -64,29 +62,29 @@ class UpdateHandler(Cleaner):
result.append(remote)
except Exception:
self.reporter.set_failed(local.base)
- self.logger.exception(f'could not load remote package {local.base}', exc_info=True)
+ self.logger.exception(f"could not load remote package {local.base}")
continue
return result
def updates_manual(self) -> List[Package]:
- '''
+ """
check for packages for which manual update has been requested
:return: list of packages which are out-of-dated
- '''
+ """
result: List[Package] = []
known_bases = {package.base for package in self.packages()}
- for fn in os.listdir(self.paths.manual):
+ for fn in self.paths.manual.iterdir():
try:
- local = Package.load(os.path.join(self.paths.manual, fn), self.pacman, self.aur_url)
+ local = Package.load(fn, self.pacman, self.aur_url)
result.append(local)
if local.base not in known_bases:
self.reporter.set_unknown(local)
else:
self.reporter.set_pending(local.base)
except Exception:
- self.logger.exception(f'could not add package from {fn}', exc_info=True)
+ self.logger.exception(f"could not add package from {fn}")
self.clear_manual()
return result
diff --git a/src/ahriman/core/sign/__init__.py b/src/ahriman/core/sign/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/sign/__init__.py
+++ b/src/ahriman/core/sign/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/sign/gpg.py b/src/ahriman/core/sign/gpg.py
index d329471f..93997cc5 100644
--- a/src/ahriman/core/sign/gpg.py
+++ b/src/ahriman/core/sign/gpg.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -18,8 +18,8 @@
# along with this program. If not, see .
#
import logging
-import os
+from pathlib import Path
from typing import List
from ahriman.core.configuration import Configuration
@@ -29,79 +29,80 @@ from ahriman.models.sign_settings import SignSettings
class GPG:
- '''
+ """
gnupg wrapper
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar default_key: default PGP key ID to use
:ivar logger: class logger
:ivar target: list of targets to sign (repository, package etc)
- '''
+ """
+
+ _check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
- self.logger = logging.getLogger('build_details')
+ """
+ self.logger = logging.getLogger("build_details")
self.config = config
- self.section = config.get_section_name('sign', architecture)
- self.target = [SignSettings.from_option(opt) for opt in config.getlist(self.section, 'target')]
- self.default_key = config.get(self.section, 'key') if self.target else ''
+ self.section = config.get_section_name("sign", architecture)
+ self.target = {SignSettings.from_option(opt) for opt in config.getlist(self.section, "target")}
+ self.default_key = config.get(self.section, "key") if self.target else ""
@property
def repository_sign_args(self) -> List[str]:
- '''
+ """
:return: command line arguments for repo-add command to sign database
- '''
+ """
if SignSettings.SignRepository not in self.target:
return []
- return ['--sign', '--key', self.default_key]
+ return ["--sign", "--key", self.default_key]
@staticmethod
- def sign_cmd(path: str, key: str) -> List[str]:
- '''
+ def sign_cmd(path: Path, key: str) -> List[str]:
+ """
gpg command to run
:param path: path to file to sign
:param key: PGP key ID
:return: gpg command with all required arguments
- '''
- return ['gpg', '-u', key, '-b', path]
+ """
+ return ["gpg", "-u", key, "-b", str(path)]
- def process(self, path: str, key: str) -> List[str]:
- '''
+ def process(self, path: Path, key: str) -> List[Path]:
+ """
gpg command wrapper
:param path: path to file to sign
:param key: PGP key ID
:return: list of generated files including original file
- '''
- check_output(
+ """
+ GPG._check_output(
*GPG.sign_cmd(path, key),
- exception=BuildFailed(path),
- cwd=os.path.dirname(path),
+ exception=BuildFailed(path.name),
logger=self.logger)
- return [path, f'{path}.sig']
+ return [path, path.parent / f"{path.name}.sig"]
- def sign_package(self, path: str, base: str) -> List[str]:
- '''
+ def sign_package(self, path: Path, base: str) -> List[Path]:
+ """
sign package if required by configuration
:param path: path to file to sign
:param base: package base required to check for key overrides
:return: list of generated files including original file
- '''
+ """
if SignSettings.SignPackages not in self.target:
return [path]
- key = self.config.get(self.section, f'key_{base}', fallback=self.default_key)
+ key = self.config.get(self.section, f"key_{base}", fallback=self.default_key)
return self.process(path, key)
- def sign_repository(self, path: str) -> List[str]:
- '''
+ def sign_repository(self, path: Path) -> List[Path]:
+ """
sign repository if required by configuration
:note: more likely you just want to pass `repository_sign_args` to repo wrapper
:param path: path to repository database
:return: list of generated files including original file
- '''
+ """
if SignSettings.SignRepository not in self.target:
return [path]
return self.process(path, self.default_key)
diff --git a/src/ahriman/core/watcher/__init__.py b/src/ahriman/core/status/__init__.py
similarity index 94%
rename from src/ahriman/core/watcher/__init__.py
rename to src/ahriman/core/status/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/watcher/__init__.py
+++ b/src/ahriman/core/status/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/watcher/client.py b/src/ahriman/core/status/client.py
similarity index 84%
rename from src/ahriman/core/watcher/client.py
rename to src/ahriman/core/status/client.py
index 98e10381..f8fa711b 100644
--- a/src/ahriman/core/watcher/client.py
+++ b/src/ahriman/core/status/client.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -27,102 +27,102 @@ from ahriman.models.package import Package
class Client:
- '''
+ """
base build status reporter client
- '''
+ """
def add(self, package: Package, status: BuildStatusEnum) -> None:
- '''
+ """
add new package with status
:param package: package properties
:param status: current package build status
- '''
+ """
- # pylint: disable=R0201
+ # pylint: disable=no-self-use
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
- '''
+ """
get package status
:param base: package base to get
:return: list of current package description and status if it has been found
- '''
+ """
del base
return []
- # pylint: disable=R0201
+ # pylint: disable=no-self-use
def get_self(self) -> BuildStatus:
- '''
+ """
get ahriman status itself
:return: current ahriman status
- '''
+ """
return BuildStatus()
def remove(self, base: str) -> None:
- '''
+ """
remove packages from watcher
:param base: package base to remove
- '''
+ """
def update(self, base: str, status: BuildStatusEnum) -> None:
- '''
+ """
update package build status. Unlike `add` it does not update package properties
:param base: package base to update
:param status: current package build status
- '''
+ """
def update_self(self, status: BuildStatusEnum) -> None:
- '''
+ """
update ahriman status itself
:param status: current ahriman status
- '''
+ """
def set_building(self, base: str) -> None:
- '''
+ """
set package status to building
:param base: package base to update
- '''
+ """
return self.update(base, BuildStatusEnum.Building)
def set_failed(self, base: str) -> None:
- '''
+ """
set package status to failed
:param base: package base to update
- '''
+ """
return self.update(base, BuildStatusEnum.Failed)
def set_pending(self, base: str) -> None:
- '''
+ """
set package status to pending
:param base: package base to update
- '''
+ """
return self.update(base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None:
- '''
+ """
set package status to success
:param package: current package properties
- '''
+ """
return self.add(package, BuildStatusEnum.Success)
def set_unknown(self, package: Package) -> None:
- '''
+ """
set package status to unknown
:param package: current package properties
- '''
+ """
return self.add(package, BuildStatusEnum.Unknown)
@staticmethod
def load(architecture: str, config: Configuration) -> Client:
- '''
+ """
load client from settings
:param architecture: repository architecture
:param config: configuration instance
:return: client according to current settings
- '''
- section = config.get_section_name('web', architecture)
- host = config.get(section, 'host', fallback=None)
- port = config.getint(section, 'port', fallback=None)
+ """
+ section = config.get_section_name("web", architecture)
+ host = config.get(section, "host", fallback=None)
+ port = config.getint(section, "port", fallback=None)
if host is None or port is None:
return Client()
- from ahriman.core.watcher.web_client import WebClient
+ from ahriman.core.status.web_client import WebClient
return WebClient(host, port)
diff --git a/src/ahriman/core/watcher/watcher.py b/src/ahriman/core/status/watcher.py
similarity index 72%
rename from src/ahriman/core/watcher/watcher.py
rename to src/ahriman/core/status/watcher.py
index 70d3888a..968587b5 100644
--- a/src/ahriman/core/watcher/watcher.py
+++ b/src/ahriman/core/status/watcher.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -19,33 +19,34 @@
#
import json
import logging
-import os
+from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
from ahriman.core.configuration import Configuration
+from ahriman.core.exceptions import UnknownPackage
from ahriman.core.repository.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
class Watcher:
- '''
+ """
package status watcher
:ivar architecture: repository architecture
:ivar known: list of known packages. For the most cases `packages` should be used instead
:ivar logger: class logger
:ivar repository: repository object
:ivar status: daemon status
- '''
+ """
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
- self.logger = logging.getLogger('http')
+ """
+ self.logger = logging.getLogger("http")
self.architecture = architecture
self.repository = Repository(architecture, config)
@@ -54,68 +55,75 @@ class Watcher:
self.status = BuildStatus()
@property
- def cache_path(self) -> str:
- '''
+ def cache_path(self) -> Path:
+ """
:return: path to dump with json cache
- '''
- return os.path.join(self.repository.paths.root, 'status_cache.json')
+ """
+ return self.repository.paths.root / "status_cache.json"
@property
def packages(self) -> List[Tuple[Package, BuildStatus]]:
- '''
+ """
:return: list of packages together with their statuses
- '''
+ """
return list(self.known.values())
def _cache_load(self) -> None:
- '''
+ """
update current state from cache
- '''
+ """
def parse_single(properties: Dict[str, Any]) -> None:
- package = Package.from_json(properties['package'])
- status = BuildStatus.from_json(properties['status'])
+ package = Package.from_json(properties["package"])
+ status = BuildStatus.from_json(properties["status"])
if package.base in self.known:
self.known[package.base] = (package, status)
- if not os.path.isfile(self.cache_path):
+ if not self.cache_path.is_file():
return
- with open(self.cache_path) as cache:
- dump = json.load(cache)
- for item in dump['packages']:
+ with self.cache_path.open() as cache:
+ try:
+ dump = json.load(cache)
+ except Exception:
+ self.logger.exception("cannot parse json from file")
+ dump = {}
+ for item in dump.get("packages", []):
try:
parse_single(item)
except Exception:
- self.logger.exception(f'cannot parse item f{item} to package', exc_info=True)
+ self.logger.exception(f"cannot parse item f{item} to package")
def _cache_save(self) -> None:
- '''
+ """
dump current cache to filesystem
- '''
+ """
dump = {
- 'packages': [
+ "packages": [
{
- 'package': package.view(),
- 'status': status.view()
+ "package": package.view(),
+ "status": status.view()
} for package, status in self.packages
]
}
try:
- with open(self.cache_path, 'w') as cache:
+ with self.cache_path.open("w") as cache:
json.dump(dump, cache)
except Exception:
- self.logger.exception('cannot dump cache', exc_info=True)
+ self.logger.exception("cannot dump cache")
def get(self, base: str) -> Tuple[Package, BuildStatus]:
- '''
+ """
get current package base build status
:return: package and its status
- '''
- return self.known[base]
+ """
+ try:
+ return self.known[base]
+ except KeyError:
+ raise UnknownPackage(base)
def load(self) -> None:
- '''
+ """
load packages from local repository. In case if last status is known, it will use it
- '''
+ """
for package in self.repository.packages():
# get status of build or assign unknown
current = self.known.get(package.base)
@@ -127,29 +135,32 @@ class Watcher:
self._cache_load()
def remove(self, base: str) -> None:
- '''
+ """
remove package base from known list if any
:param base: package base
- '''
+ """
self.known.pop(base, None)
self._cache_save()
def update(self, base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
- '''
+ """
update package status and description
:param base: package base to update
:param status: new build status
:param package: optional new package description. In case if not set current properties will be used
- '''
+ """
if package is None:
- package, _ = self.known[base]
+ try:
+ package, _ = self.known[base]
+ except KeyError:
+ raise UnknownPackage(base)
full_status = BuildStatus(status)
self.known[base] = (package, full_status)
self._cache_save()
def update_self(self, status: BuildStatusEnum) -> None:
- '''
+ """
update service status
:param status: new service status
- '''
+ """
self.status = BuildStatus(status)
diff --git a/src/ahriman/core/watcher/web_client.py b/src/ahriman/core/status/web_client.py
similarity index 67%
rename from src/ahriman/core/watcher/web_client.py
rename to src/ahriman/core/status/web_client.py
index 5e6e2d82..2a3c2ea3 100644
--- a/src/ahriman/core/watcher/web_client.py
+++ b/src/ahriman/core/status/web_client.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -18,93 +18,93 @@
# along with this program. If not, see .
#
import logging
-from typing import List, Optional, Tuple
-
import requests
-from ahriman.core.watcher.client import Client
+from typing import List, Optional, Tuple
+
+from ahriman.core.status.client import Client
from ahriman.models.build_status import BuildStatusEnum, BuildStatus
from ahriman.models.package import Package
class WebClient(Client):
- '''
+ """
build status reporter web client
:ivar host: host of web service
:ivar logger: class logger
:ivar port: port of web service
- '''
+ """
def __init__(self, host: str, port: int) -> None:
- '''
+ """
default constructor
:param host: host of web service
:param port: port of web service
- '''
- self.logger = logging.getLogger('http')
+ """
+ self.logger = logging.getLogger("http")
self.host = host
self.port = port
def _ahriman_url(self) -> str:
- '''
+ """
url generator
:return: full url for web service for ahriman service itself
- '''
- return f'http://{self.host}:{self.port}/api/v1/ahriman'
+ """
+ return f"http://{self.host}:{self.port}/api/v1/ahriman"
- def _package_url(self, base: str = '') -> str:
- '''
+ def _package_url(self, base: str = "") -> str:
+ """
url generator
:param base: package base to generate url
:return: full url of web service for specific package base
- '''
- return f'http://{self.host}:{self.port}/api/v1/packages/{base}'
+ """
+ return f"http://{self.host}:{self.port}/api/v1/packages/{base}"
def add(self, package: Package, status: BuildStatusEnum) -> None:
- '''
+ """
add new package with status
:param package: package properties
:param status: current package build status
- '''
+ """
payload = {
- 'status': status.value,
- 'package': package.view()
+ "status": status.value,
+ "package": package.view()
}
try:
response = requests.post(self._package_url(package.base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
- self.logger.exception(f'could not add {package.base}: {e.response.text}', exc_info=True)
+ self.logger.exception(f"could not add {package.base}: {e.response.text}")
except Exception:
- self.logger.exception(f'could not add {package.base}', exc_info=True)
+ self.logger.exception(f"could not add {package.base}")
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]:
- '''
+ """
get package status
:param base: package base to get
:return: list of current package description and status if it has been found
- '''
+ """
try:
- response = requests.get(self._package_url(base or ''))
+ response = requests.get(self._package_url(base or ""))
response.raise_for_status()
status_json = response.json()
return [
- (Package.from_json(package['package']), BuildStatus.from_json(package['status']))
+ (Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
for package in status_json
]
except requests.exceptions.HTTPError as e:
- self.logger.exception(f'could not get {base}: {e.response.text}', exc_info=True)
+ self.logger.exception(f"could not get {base}: {e.response.text}")
except Exception:
- self.logger.exception(f'could not get {base}', exc_info=True)
+ self.logger.exception(f"could not get {base}")
return []
def get_self(self) -> BuildStatus:
- '''
+ """
get ahriman status itself
:return: current ahriman status
- '''
+ """
try:
response = requests.get(self._ahriman_url())
response.raise_for_status()
@@ -112,51 +112,51 @@ class WebClient(Client):
status_json = response.json()
return BuildStatus.from_json(status_json)
except requests.exceptions.HTTPError as e:
- self.logger.exception(f'could not get service status: {e.response.text}', exc_info=True)
+ self.logger.exception(f"could not get service status: {e.response.text}")
except Exception:
- self.logger.exception('could not get service status', exc_info=True)
+ self.logger.exception("could not get service status")
return BuildStatus()
def remove(self, base: str) -> None:
- '''
+ """
remove packages from watcher
:param base: basename to remove
- '''
+ """
try:
response = requests.delete(self._package_url(base))
response.raise_for_status()
except requests.exceptions.HTTPError as e:
- self.logger.exception(f'could not delete {base}: {e.response.text}', exc_info=True)
+ self.logger.exception(f"could not delete {base}: {e.response.text}")
except Exception:
- self.logger.exception(f'could not delete {base}', exc_info=True)
+ self.logger.exception(f"could not delete {base}")
def update(self, base: str, status: BuildStatusEnum) -> None:
- '''
+ """
update package build status. Unlike `add` it does not update package properties
:param base: package base to update
:param status: current package build status
- '''
- payload = {'status': status.value}
+ """
+ payload = {"status": status.value}
try:
response = requests.post(self._package_url(base), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
- self.logger.exception(f'could not update {base}: {e.response.text}', exc_info=True)
+ self.logger.exception(f"could not update {base}: {e.response.text}")
except Exception:
- self.logger.exception(f'could not update {base}', exc_info=True)
+ self.logger.exception(f"could not update {base}")
def update_self(self, status: BuildStatusEnum) -> None:
- '''
+ """
update ahriman status itself
:param status: current ahriman status
- '''
- payload = {'status': status.value}
+ """
+ payload = {"status": status.value}
try:
response = requests.post(self._ahriman_url(), json=payload)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
- self.logger.exception(f'could not update service status: {e.response.text}', exc_info=True)
+ self.logger.exception(f"could not update service status: {e.response.text}")
except Exception:
- self.logger.exception('could not update service status', exc_info=True)
+ self.logger.exception("could not update service status")
diff --git a/src/ahriman/core/tree.py b/src/ahriman/core/tree.py
index 00943338..56b1ac41 100644
--- a/src/ahriman/core/tree.py
+++ b/src/ahriman/core/tree.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -22,74 +22,90 @@ from __future__ import annotations
import shutil
import tempfile
-from typing import Iterable, List, Set
+from pathlib import Path
+from typing import Iterable, List, Set, Type
from ahriman.core.build_tools.task import Task
from ahriman.models.package import Package
class Leaf:
- '''
+ """
tree leaf implementation
:ivar dependencies: list of package dependencies
:ivar package: leaf package properties
- '''
+ """
- def __init__(self, package: Package) -> None:
- '''
+ def __init__(self, package: Package, dependencies: Set[str]) -> None:
+ """
default constructor
:param package: package properties
- '''
+ :param dependencies: package dependencies
+ """
self.package = package
- self.dependencies: Set[str] = set()
+ self.dependencies = dependencies
@property
def items(self) -> Iterable[str]:
- '''
+ """
:return: packages containing in this leaf
- '''
+ """
return self.package.packages.keys()
+ @classmethod
+ def load(cls: Type[Leaf], package: Package) -> Leaf:
+ """
+ load leaf from package with dependencies
+ :param package: package properties
+ :return: loaded class
+ """
+ clone_dir = Path(tempfile.mkdtemp())
+ try:
+ Task.fetch(clone_dir, package.git_url)
+ dependencies = Package.dependencies(clone_dir)
+ finally:
+ shutil.rmtree(clone_dir, ignore_errors=True)
+ return cls(package, dependencies)
+
def is_root(self, packages: Iterable[Leaf]) -> bool:
- '''
+ """
check if package depends on any other package from list of not
:param packages: list of known leaves
:return: True if any of packages is dependency of the leaf, False otherwise
- '''
+ """
for leaf in packages:
if self.dependencies.intersection(leaf.items):
return False
return True
- def load_dependencies(self) -> None:
- '''
- load dependencies for the leaf
- '''
- clone_dir = tempfile.mkdtemp()
- try:
- Task.fetch(clone_dir, self.package.git_url)
- self.dependencies = Package.dependencies(clone_dir)
- finally:
- shutil.rmtree(clone_dir, ignore_errors=True)
-
class Tree:
- '''
+ """
dependency tree implementation
:ivar leaves: list of tree leaves
- '''
+ """
- def __init__(self) -> None:
- '''
+ def __init__(self, leaves: List[Leaf]) -> None:
+ """
default constructor
- '''
- self.leaves: List[Leaf] = []
+ :param leaves: leaves to build the tree
+ """
+ self.leaves = leaves
+
+ @classmethod
+ def load(cls: Type[Tree], packages: Iterable[Package]) -> Tree:
+ """
+ load tree from packages
+ :param packages: packages list
+ :return: loaded class
+ """
+ return cls([Leaf.load(package) for package in packages])
def levels(self) -> List[List[Package]]:
- '''
+ """
get build levels starting from the packages which do not require any other package to build
:return: list of packages lists
- '''
+ """
result: List[List[Package]] = []
unprocessed = self.leaves[:]
@@ -98,13 +114,3 @@ class Tree:
unprocessed = [leaf for leaf in unprocessed if not leaf.is_root(unprocessed)]
return result
-
- def load(self, packages: Iterable[Package]) -> None:
- '''
- load tree from packages
- :param packages: packages list
- '''
- for package in packages:
- leaf = Leaf(package)
- leaf.load_dependencies()
- self.leaves.append(leaf)
diff --git a/src/ahriman/core/upload/__init__.py b/src/ahriman/core/upload/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/core/upload/__init__.py
+++ b/src/ahriman/core/upload/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/core/upload/rsync.py b/src/ahriman/core/upload/rsync.py
index 98e42b0b..65434765 100644
--- a/src/ahriman/core/upload/rsync.py
+++ b/src/ahriman/core/upload/rsync.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,32 +17,44 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
+from pathlib import Path
+
from ahriman.core.configuration import Configuration
from ahriman.core.upload.uploader import Uploader
from ahriman.core.util import check_output
class Rsync(Uploader):
- '''
+ """
rsync wrapper
:ivar remote: remote address to sync
- '''
+ """
+
+ _check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Uploader.__init__(self, architecture, config)
- section = config.get_section_name('rsync', architecture)
- self.remote = config.get(section, 'remote')
+ section = config.get_section_name("rsync", architecture)
+ self.remote = config.get(section, "remote")
- def sync(self, path: str) -> None:
- '''
+ def sync(self, path: Path) -> None:
+ """
sync data to remote server
:param path: local path to sync
- '''
- check_output('rsync', '--archive', '--verbose', '--compress', '--partial', '--delete', path, self.remote,
- exception=None,
- logger=self.logger)
+ """
+ Rsync._check_output(
+ "rsync",
+ "--archive",
+ "--verbose",
+ "--compress",
+ "--partial",
+ "--delete",
+ str(path),
+ self.remote,
+ exception=None,
+ logger=self.logger)
diff --git a/src/ahriman/core/upload/s3.py b/src/ahriman/core/upload/s3.py
index 7085b438..f483d952 100644
--- a/src/ahriman/core/upload/s3.py
+++ b/src/ahriman/core/upload/s3.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,33 +17,37 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
+from pathlib import Path
+
from ahriman.core.configuration import Configuration
from ahriman.core.upload.uploader import Uploader
from ahriman.core.util import check_output
class S3(Uploader):
- '''
+ """
aws-cli wrapper
:ivar bucket: full bucket name
- '''
+ """
+
+ _check_output = check_output
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
+ """
Uploader.__init__(self, architecture, config)
- section = config.get_section_name('s3', architecture)
- self.bucket = config.get(section, 'bucket')
+ section = config.get_section_name("s3", architecture)
+ self.bucket = config.get(section, "bucket")
- def sync(self, path: str) -> None:
- '''
+ def sync(self, path: Path) -> None:
+ """
sync data to remote server
:param path: local path to sync
- '''
+ """
# TODO rewrite to boto, but it is bullshit
- check_output('aws', 's3', 'sync', '--quiet', '--delete', path, self.bucket,
- exception=None,
- logger=self.logger)
+ S3._check_output("aws", "s3", "sync", "--quiet", "--delete", str(path), self.bucket,
+ exception=None,
+ logger=self.logger)
diff --git a/src/ahriman/core/upload/uploader.py b/src/ahriman/core/upload/uploader.py
index bfa6d0c8..d9266143 100644
--- a/src/ahriman/core/upload/uploader.py
+++ b/src/ahriman/core/upload/uploader.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -19,38 +19,40 @@
#
import logging
+from pathlib import Path
+
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import SyncFailed
from ahriman.models.upload_settings import UploadSettings
class Uploader:
- '''
+ """
base remote sync class
:ivar architecture: repository architecture
:ivar config: configuration instance
:ivar logger: application logger
- '''
+ """
def __init__(self, architecture: str, config: Configuration) -> None:
- '''
+ """
default constructor
:param architecture: repository architecture
:param config: configuration instance
- '''
- self.logger = logging.getLogger('builder')
+ """
+ self.logger = logging.getLogger("builder")
self.architecture = architecture
self.config = config
@staticmethod
- def run(architecture: str, config: Configuration, target: str, path: str) -> None:
- '''
+ def run(architecture: str, config: Configuration, target: str, path: Path) -> None:
+ """
run remote sync
:param architecture: repository architecture
:param config: configuration instance
:param target: target to run sync (e.g. s3)
:param path: local path to sync
- '''
+ """
provider = UploadSettings.from_option(target)
if provider == UploadSettings.Rsync:
from ahriman.core.upload.rsync import Rsync
@@ -64,11 +66,11 @@ class Uploader:
try:
uploader.sync(path)
except Exception:
- uploader.logger.exception('remote sync failed', exc_info=True)
+ uploader.logger.exception(f"remote sync failed for {provider.name}")
raise SyncFailed()
- def sync(self, path: str) -> None:
- '''
+ def sync(self, path: Path) -> None:
+ """
sync data to remote server
:param path: local path to sync
- '''
+ """
diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py
index 8ce886a9..6045b280 100644
--- a/src/ahriman/core/util.py
+++ b/src/ahriman/core/util.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -21,74 +21,74 @@ import datetime
import subprocess
from logging import Logger
+from pathlib import Path
from typing import Optional
from ahriman.core.exceptions import InvalidOption
def check_output(*args: str, exception: Optional[Exception],
- cwd: Optional[str] = None, stderr: int = subprocess.STDOUT,
- logger: Optional[Logger] = None) -> str:
- '''
+ cwd: Optional[Path] = None, logger: Optional[Logger] = None) -> str:
+ """
subprocess wrapper
:param args: command line arguments
:param exception: exception which has to be reraised instead of default subprocess exception
:param cwd: current working directory
- :param stderr: standard error output mode
:param logger: logger to log command result if required
:return: command output
- '''
+ """
try:
- result = subprocess.check_output(args, cwd=cwd, stderr=stderr).decode('utf8').strip()
+ result = subprocess.check_output(args, cwd=cwd, stderr=subprocess.STDOUT).decode("utf8").strip()
if logger is not None:
for line in result.splitlines():
logger.debug(line)
except subprocess.CalledProcessError as e:
if e.output is not None and logger is not None:
- for line in e.output.decode('utf8').splitlines():
+ for line in e.output.decode("utf8").splitlines():
logger.debug(line)
raise exception or e
return result
-def package_like(filename: str) -> bool:
- '''
+def package_like(filename: Path) -> bool:
+ """
check if file looks like package
:param filename: name of file to check
:return: True in case if name contains `.pkg.` and not signature, False otherwise
- '''
- return '.pkg.' in filename and not filename.endswith('.sig')
+ """
+ name = filename.name
+ return ".pkg." in name and not name.endswith(".sig")
def pretty_datetime(timestamp: Optional[int]) -> str:
- '''
+ """
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
- '''
- return '' if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
+ """
+ return "" if timestamp is None else datetime.datetime.utcfromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
def pretty_size(size: Optional[float], level: int = 0) -> str:
- '''
+ """
convert size to string
:param size: size to convert
:param level: represents current units, 0 is B, 1 is KiB etc
:return: pretty printable size as string
- '''
+ """
def str_level() -> str:
if level == 0:
- return 'B'
+ return "B"
if level == 1:
- return 'KiB'
+ return "KiB"
if level == 2:
- return 'MiB'
+ return "MiB"
if level == 3:
- return 'GiB'
- raise InvalidOption(level) # I hope it will not be more than 1024 GiB
+ return "GiB"
+ raise InvalidOption(level) # must never happen actually
if size is None:
- return ''
- if size < 1024:
- return f'{round(size, 2)} {str_level()}'
+ return ""
+ if size < 1024 or level == 3:
+ return f"{size:.1f} {str_level()}"
return pretty_size(size / 1024, level + 1)
diff --git a/src/ahriman/models/__init__.py b/src/ahriman/models/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/models/__init__.py
+++ b/src/ahriman/models/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py
index 2d3b4963..7489ff86 100644
--- a/src/ahriman/models/build_status.py
+++ b/src/ahriman/models/build_status.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -28,83 +28,93 @@ from ahriman.core.util import pretty_datetime
class BuildStatusEnum(Enum):
- '''
+ """
build status enumeration
:cvar Unknown: build status is unknown
:cvar Pending: package is out-of-dated and will be built soon
:cvar Building: package is building right now
:cvar Failed: package build failed
:cvar Success: package has been built without errors
- '''
+ """
- Unknown = 'unknown'
- Pending = 'pending'
- Building = 'building'
- Failed = 'failed'
- Success = 'success'
+ Unknown = "unknown"
+ Pending = "pending"
+ Building = "building"
+ Failed = "failed"
+ Success = "success"
def badges_color(self) -> str:
- '''
+ """
convert itself to shield.io badges color
:return: shields.io color
- '''
+ """
if self == BuildStatusEnum.Pending:
- return 'yellow'
+ return "yellow"
if self == BuildStatusEnum.Building:
- return 'yellow'
+ return "yellow"
if self == BuildStatusEnum.Failed:
- return 'critical'
+ return "critical"
if self == BuildStatusEnum.Success:
- return 'success'
- return 'inactive'
+ return "success"
+ return "inactive"
class BuildStatus:
- '''
+ """
build status holder
:ivar status: build status
:ivar _timestamp: build status update time
- '''
+ """
def __init__(self, status: Union[BuildStatusEnum, str, None] = None,
timestamp: Optional[int] = None) -> None:
- '''
+ """
default constructor
:param status: current build status if known. `BuildStatusEnum.Unknown` will be used if not set
:param timestamp: build status timestamp. Current timestamp will be used if not set
- '''
+ """
self.status = BuildStatusEnum(status) if status else BuildStatusEnum.Unknown
self.timestamp = timestamp or int(datetime.datetime.utcnow().timestamp())
@classmethod
def from_json(cls: Type[BuildStatus], dump: Dict[str, Any]) -> BuildStatus:
- '''
+ """
construct status properties from json dump
:param dump: json dump body
:return: status properties
- '''
- return cls(dump.get('status'), dump.get('timestamp'))
+ """
+ return cls(dump.get("status"), dump.get("timestamp"))
def pretty_print(self) -> str:
- '''
+ """
generate pretty string representation
:return: print-friendly string
- '''
- return f'{self.status.value} ({pretty_datetime(self.timestamp)})'
+ """
+ return f"{self.status.value} ({pretty_datetime(self.timestamp)})"
def view(self) -> Dict[str, Any]:
- '''
+ """
generate json status view
:return: json-friendly dictionary
- '''
+ """
return {
- 'status': self.status.value,
- 'timestamp': self.timestamp
+ "status": self.status.value,
+ "timestamp": self.timestamp
}
+ def __eq__(self, other: Any) -> bool:
+ """
+ compare object to other
+ :param other: other object to compare
+ :return: True in case if objects are equal
+ """
+ if not isinstance(other, BuildStatus):
+ return False
+ return self.status == other.status and self.timestamp == other.timestamp
+
def __repr__(self) -> str:
- '''
+ """
generate string representation of object
:return: unique string representation
- '''
- return f'BuildStatus(status={self.status.value}, timestamp={self.timestamp})'
+ """
+ return f"BuildStatus(status={self.status.value}, timestamp={self.timestamp})"
diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py
index a6c597f7..f028efed 100644
--- a/src/ahriman/models/package.py
+++ b/src/ahriman/models/package.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -21,12 +21,12 @@ from __future__ import annotations
import aur # type: ignore
import logging
-import os
from dataclasses import asdict, dataclass
+from pathlib import Path
from pyalpm import vercmp # type: ignore
from srcinfo.parse import parse_srcinfo # type: ignore
-from typing import Any, Dict, List, Optional, Set, Type
+from typing import Any, Dict, List, Optional, Set, Type, Union
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import InvalidPackageInfo
@@ -37,158 +37,166 @@ from ahriman.models.repository_paths import RepositoryPaths
@dataclass
class Package:
- '''
+ """
package properties representation
- :ivar aurl_url: AUR root url
+ :ivar aur_url: AUR root url
:ivar base: package base name
:ivar packages: map of package names to their properties. Filled only on load from archive
:ivar version: package full version
- '''
+ """
base: str
version: str
aur_url: str
packages: Dict[str, PackageDescription]
+ _check_output = check_output
+
@property
def git_url(self) -> str:
- '''
+ """
:return: package git url to clone
- '''
- return f'{self.aur_url}/{self.base}.git'
+ """
+ return f"{self.aur_url}/{self.base}.git"
@property
def is_single_package(self) -> bool:
- '''
+ """
:return: true in case if this base has only one package with the same name
- '''
+ """
return self.base in self.packages and len(self.packages) == 1
@property
def is_vcs(self) -> bool:
- '''
+ """
:return: True in case if package base looks like VCS package and false otherwise
- '''
- return self.base.endswith('-bzr') \
- or self.base.endswith('-csv')\
- or self.base.endswith('-darcs')\
- or self.base.endswith('-git')\
- or self.base.endswith('-hg')\
- or self.base.endswith('-svn')
+ """
+ return self.base.endswith("-bzr") \
+ or self.base.endswith("-csv")\
+ or self.base.endswith("-darcs")\
+ or self.base.endswith("-git")\
+ or self.base.endswith("-hg")\
+ or self.base.endswith("-svn")
@property
def web_url(self) -> str:
- '''
+ """
:return: package AUR url
- '''
- return f'{self.aur_url}/packages/{self.base}'
+ """
+ return f"{self.aur_url}/packages/{self.base}"
@classmethod
- def from_archive(cls: Type[Package], path: str, pacman: Pacman, aur_url: str) -> Package:
- '''
+ def from_archive(cls: Type[Package], path: Path, pacman: Pacman, aur_url: str) -> Package:
+ """
construct package properties from package archive
:param path: path to package archive
:param pacman: alpm wrapper instance
:param aur_url: AUR root url
:return: package properties
- '''
- package = pacman.handle.load_pkg(path)
- properties = PackageDescription(package.size, package.builddate, os.path.basename(path), package.isize)
+ """
+ package = pacman.handle.load_pkg(str(path))
+ properties = PackageDescription(package.size, package.builddate, path.name, package.isize)
return cls(package.base, package.version, aur_url, {package.name: properties})
@classmethod
def from_aur(cls: Type[Package], name: str, aur_url: str) -> Package:
- '''
+ """
construct package properties from AUR page
:param name: package name (either base or normal name)
:param aur_url: AUR root url
:return: package properties
- '''
+ """
package = aur.info(name)
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
@classmethod
- def from_build(cls: Type[Package], path: str, aur_url: str) -> Package:
- '''
+ def from_build(cls: Type[Package], path: Path, aur_url: str) -> Package:
+ """
construct package properties from sources directory
:param path: path to package sources directory
:param aur_url: AUR root url
:return: package properties
- '''
- with open(os.path.join(path, '.SRCINFO')) as srcinfo_file:
- srcinfo, errors = parse_srcinfo(srcinfo_file.read())
+ """
+ srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
if errors:
raise InvalidPackageInfo(errors)
- packages = {key: PackageDescription() for key in srcinfo['packages']}
- version = cls.full_version(srcinfo.get('epoch'), srcinfo['pkgver'], srcinfo['pkgrel'])
+ packages = {key: PackageDescription() for key in srcinfo["packages"]}
+ version = cls.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
- return cls(srcinfo['pkgbase'], version, aur_url, packages)
+ return cls(srcinfo["pkgbase"], version, aur_url, packages)
@classmethod
def from_json(cls: Type[Package], dump: Dict[str, Any]) -> Package:
- '''
+ """
construct package properties from json dump
:param dump: json dump body
:return: package properties
- '''
+ """
packages = {
key: PackageDescription(**value)
- for key, value in dump.get('packages', {}).items()
+ for key, value in dump.get("packages", {}).items()
}
return Package(
- base=dump['base'],
- version=dump['version'],
- aur_url=dump['aur_url'],
+ base=dump["base"],
+ version=dump["version"],
+ aur_url=dump["aur_url"],
packages=packages)
@staticmethod
- def dependencies(path: str) -> Set[str]:
- '''
+ def dependencies(path: Path) -> Set[str]:
+ """
load dependencies from package sources
:param path: path to package sources directory
:return: list of package dependencies including makedepends array, but excluding packages from this base
- '''
- with open(os.path.join(path, '.SRCINFO')) as srcinfo_file:
- srcinfo, errors = parse_srcinfo(srcinfo_file.read())
+ """
+ # additional function to remove versions from dependencies
+ def trim_version(name: str) -> str:
+ for symbol in ("<", "=", ">"):
+ name = name.split(symbol)[0]
+ return name
+
+ srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
if errors:
raise InvalidPackageInfo(errors)
- makedepends = srcinfo.get('makedepends', [])
+ makedepends = srcinfo.get("makedepends", [])
# sum over each package
- depends: List[str] = srcinfo.get('depends', [])
- for package in srcinfo['packages'].values():
- depends.extend(package.get('depends', []))
+ depends: List[str] = srcinfo.get("depends", [])
+ for package in srcinfo["packages"].values():
+ depends.extend(package.get("depends", []))
# we are not interested in dependencies inside pkgbase
- packages = set(srcinfo['packages'].keys())
- return set(depends + makedepends) - packages
+ packages = set(srcinfo["packages"].keys())
+ full_list = set(depends + makedepends) - packages
+ return {trim_version(package_name) for package_name in full_list}
@staticmethod
def full_version(epoch: Optional[str], pkgver: str, pkgrel: str) -> str:
- '''
+ """
generate full version from components
:param epoch: package epoch if any
:param pkgver: package version
:param pkgrel: package release version (archlinux specific)
:return: generated version
- '''
- prefix = f'{epoch}:' if epoch else ''
- return f'{prefix}{pkgver}-{pkgrel}'
+ """
+ prefix = f"{epoch}:" if epoch else ""
+ return f"{prefix}{pkgver}-{pkgrel}"
@staticmethod
- def load(path: str, pacman: Pacman, aur_url: str) -> Package:
- '''
+ def load(path: Union[Path, str], pacman: Pacman, aur_url: str) -> Package:
+ """
package constructor from available sources
:param path: one of path to sources directory, path to archive or package name/base
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
- '''
+ """
try:
- if os.path.isdir(path):
- package: Package = Package.from_build(path, aur_url)
- elif os.path.exists(path):
- package = Package.from_archive(path, pacman, aur_url)
+ maybe_path = Path(path)
+ if maybe_path.is_dir():
+ package: Package = Package.from_build(maybe_path, aur_url)
+ elif maybe_path.is_file():
+ package = Package.from_archive(maybe_path, pacman, aur_url)
else:
- package = Package.from_aur(path, aur_url)
+ package = Package.from_aur(str(path), aur_url)
return package
except InvalidPackageInfo:
raise
@@ -196,52 +204,62 @@ class Package:
raise InvalidPackageInfo(str(e))
def actual_version(self, paths: RepositoryPaths) -> str:
- '''
+ """
additional method to handle VCS package versions
:param paths: repository paths instance
:return: package version if package is not VCS and current version according to VCS otherwise
- '''
+ """
if not self.is_vcs:
return self.version
from ahriman.core.build_tools.task import Task
- clone_dir = os.path.join(paths.cache, self.base)
- logger = logging.getLogger('build_details')
+ clone_dir = paths.cache / self.base
+ logger = logging.getLogger("build_details")
Task.fetch(clone_dir, self.git_url)
- # update pkgver first
- check_output('makepkg', '--nodeps', '--nobuild', exception=None, cwd=clone_dir, logger=logger)
- # generate new .SRCINFO and put it to parser
- srcinfo_source = check_output('makepkg', '--printsrcinfo', exception=None, cwd=clone_dir, logger=logger)
- srcinfo, errors = parse_srcinfo(srcinfo_source)
- if errors:
- raise InvalidPackageInfo(errors)
+ try:
+ # update pkgver first
+ Package._check_output("makepkg", "--nodeps", "--nobuild", exception=None, cwd=clone_dir, logger=logger)
+ # generate new .SRCINFO and put it to parser
+ srcinfo_source = Package._check_output(
+ "makepkg",
+ "--printsrcinfo",
+ exception=None,
+ cwd=clone_dir,
+ logger=logger)
+ srcinfo, errors = parse_srcinfo(srcinfo_source)
+ if errors:
+ raise InvalidPackageInfo(errors)
- return self.full_version(srcinfo.get('epoch'), srcinfo['pkgver'], srcinfo['pkgrel'])
+ return self.full_version(srcinfo.get("epoch"), srcinfo["pkgver"], srcinfo["pkgrel"])
+ except Exception:
+ logger.exception("cannot determine version of VCS package, make sure that you have VCS tools installed")
+
+ return self.version
def is_outdated(self, remote: Package, paths: RepositoryPaths) -> bool:
- '''
+ """
check if package is out-of-dated
:param remote: package properties from remote source
:param paths: repository paths instance. Required for VCS packages cache
:return: True if the package is out-of-dated and False otherwise
- '''
+ """
remote_version = remote.actual_version(paths) # either normal version or updated VCS
result: int = vercmp(self.version, remote_version)
return result < 0
def pretty_print(self) -> str:
- '''
+ """
generate pretty string representation
:return: print-friendly string
- '''
- details = '' if self.is_single_package else f''' ({' '.join(sorted(self.packages.keys()))})'''
- return f'{self.base}{details}'
+ """
+ details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})"""
+ return f"{self.base}{details}"
def view(self) -> Dict[str, Any]:
- '''
+ """
generate json package view
:return: json-friendly dictionary
- '''
+ """
return asdict(self)
diff --git a/src/ahriman/models/package_desciption.py b/src/ahriman/models/package_desciption.py
index 28ded82a..4e98adf0 100644
--- a/src/ahriman/models/package_desciption.py
+++ b/src/ahriman/models/package_desciption.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -18,20 +18,28 @@
# along with this program. If not, see .
#
from dataclasses import dataclass
+from pathlib import Path
from typing import Optional
@dataclass
class PackageDescription:
- '''
+ """
package specific properties
:ivar archive_size: package archive size
:ivar build_date: package build date
:ivar filename: package archive name
:ivar installed_size: package installed size
- '''
+ """
archive_size: Optional[int] = None
build_date: Optional[int] = None
filename: Optional[str] = None
installed_size: Optional[int] = None
+
+ @property
+ def filepath(self) -> Optional[Path]:
+ """
+ :return: path object for current filename
+ """
+ return Path(self.filename) if self.filename is not None else None
diff --git a/src/ahriman/models/report_settings.py b/src/ahriman/models/report_settings.py
index 51e30305..f13bee33 100644
--- a/src/ahriman/models/report_settings.py
+++ b/src/ahriman/models/report_settings.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -25,20 +25,20 @@ from ahriman.core.exceptions import InvalidOption
class ReportSettings(Enum):
- '''
+ """
report targets enumeration
:cvar HTML: html report generation
- '''
+ """
HTML = auto()
@staticmethod
def from_option(value: str) -> ReportSettings:
- '''
+ """
construct value from configuration
:param value: configuration value
:return: parsed value
- '''
- if value.lower() in ('html',):
+ """
+ if value.lower() in ("html",):
return ReportSettings.HTML
raise InvalidOption(value)
diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py
index 36bb9074..dcfe8cf0 100644
--- a/src/ahriman/models/repository_paths.py
+++ b/src/ahriman/models/repository_paths.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,72 +17,72 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-import os
+from pathlib import Path
from dataclasses import dataclass
@dataclass
class RepositoryPaths:
- '''
+ """
repository paths holder. For the most operations with paths you want to use this object
:ivar root: repository root (i.e. ahriman home)
:ivar architecture: repository architecture
- '''
+ """
- root: str
+ root: Path
architecture: str
@property
- def cache(self) -> str:
- '''
+ def cache(self) -> Path:
+ """
:return: directory for packages cache (mainly used for VCS packages)
- '''
- return os.path.join(self.root, 'cache')
+ """
+ return self.root / "cache"
@property
- def chroot(self) -> str:
- '''
+ def chroot(self) -> Path:
+ """
:return: directory for devtools chroot
- '''
- # for the chroot directory devtools will create own tree and we don't have to specify architecture here
- return os.path.join(self.root, 'chroot')
+ """
+ # for the chroot directory devtools will create own tree and we don"t have to specify architecture here
+ return self.root / "chroot"
@property
- def manual(self) -> str:
- '''
+ def manual(self) -> Path:
+ """
:return: directory for manual updates (i.e. from add command)
- '''
- return os.path.join(self.root, 'manual', self.architecture)
+ """
+ return self.root / "manual" / self.architecture
@property
- def packages(self) -> str:
- '''
+ def packages(self) -> Path:
+ """
:return: directory for built packages
- '''
- return os.path.join(self.root, 'packages', self.architecture)
+ """
+ return self.root / "packages" / self.architecture
@property
- def repository(self) -> str:
- '''
+ def repository(self) -> Path:
+ """
:return: repository directory
- '''
- return os.path.join(self.root, 'repository', self.architecture)
+ """
+ return self.root / "repository" / self.architecture
@property
- def sources(self) -> str:
- '''
+ def sources(self) -> Path:
+ """
:return: directory for downloaded PKGBUILDs for current build
- '''
- return os.path.join(self.root, 'sources', self.architecture)
+ """
+ return self.root / "sources" / self.architecture
def create_tree(self) -> None:
- '''
+ """
create ahriman working tree
- '''
- os.makedirs(self.cache, mode=0o755, exist_ok=True)
- os.makedirs(self.chroot, mode=0o755, exist_ok=True)
- os.makedirs(self.manual, mode=0o755, exist_ok=True)
- os.makedirs(self.packages, mode=0o755, exist_ok=True)
- os.makedirs(self.repository, mode=0o755, exist_ok=True)
- os.makedirs(self.sources, mode=0o755, exist_ok=True)
+ """
+ self.cache.mkdir(mode=0o755, parents=True, exist_ok=True)
+ self.chroot.mkdir(mode=0o755, parents=True, exist_ok=True)
+ self.manual.mkdir(mode=0o755, parents=True, exist_ok=True)
+ self.packages.mkdir(mode=0o755, parents=True, exist_ok=True)
+ self.repository.mkdir(mode=0o755, parents=True, exist_ok=True)
+ self.sources.mkdir(mode=0o755, parents=True, exist_ok=True)
diff --git a/src/ahriman/models/sign_settings.py b/src/ahriman/models/sign_settings.py
index 5d625109..2919f720 100644
--- a/src/ahriman/models/sign_settings.py
+++ b/src/ahriman/models/sign_settings.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -25,24 +25,24 @@ from ahriman.core.exceptions import InvalidOption
class SignSettings(Enum):
- '''
+ """
sign targets enumeration
:cvar SignPackages: sign each package
:cvar SignRepository: sign repository database file
- '''
+ """
SignPackages = auto()
SignRepository = auto()
@staticmethod
def from_option(value: str) -> SignSettings:
- '''
+ """
construct value from configuration
:param value: configuration value
:return: parsed value
- '''
- if value.lower() in ('package', 'packages', 'sign-package'):
+ """
+ if value.lower() in ("package", "packages", "sign-package"):
return SignSettings.SignPackages
- if value.lower() in ('repository', 'sign-repository'):
+ if value.lower() in ("repository", "sign-repository"):
return SignSettings.SignRepository
raise InvalidOption(value)
diff --git a/src/ahriman/models/upload_settings.py b/src/ahriman/models/upload_settings.py
index 55e72447..a892353a 100644
--- a/src/ahriman/models/upload_settings.py
+++ b/src/ahriman/models/upload_settings.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -25,24 +25,24 @@ from ahriman.core.exceptions import InvalidOption
class UploadSettings(Enum):
- '''
+ """
remote synchronization targets enumeration
:cvar Rsync: sync via rsync
:cvar S3: sync to Amazon S3
- '''
+ """
Rsync = auto()
S3 = auto()
@staticmethod
def from_option(value: str) -> UploadSettings:
- '''
+ """
construct value from configuration
:param value: configuration value
:return: parsed value
- '''
- if value.lower() in ('rsync',):
+ """
+ if value.lower() in ("rsync",):
return UploadSettings.Rsync
- if value.lower() in ('s3',):
+ if value.lower() in ("s3",):
return UploadSettings.S3
raise InvalidOption(value)
diff --git a/src/ahriman/version.py b/src/ahriman/version.py
index ed2884da..2d8055af 100644
--- a/src/ahriman/version.py
+++ b/src/ahriman/version.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
#
-__version__ = '0.15.0'
+__version__ = "0.15.0"
diff --git a/src/ahriman/web/__init__.py b/src/ahriman/web/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/web/__init__.py
+++ b/src/ahriman/web/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/web/middlewares/__init__.py b/src/ahriman/web/middlewares/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/web/middlewares/__init__.py
+++ b/src/ahriman/web/middlewares/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/web/middlewares/exception_handler.py b/src/ahriman/web/middlewares/exception_handler.py
index ce263af6..3d20103e 100644
--- a/src/ahriman/web/middlewares/exception_handler.py
+++ b/src/ahriman/web/middlewares/exception_handler.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -28,11 +28,11 @@ HandlerType = Callable[[Request], Awaitable[StreamResponse]]
def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaitable[StreamResponse]]:
- '''
+ """
exception handler middleware. Just log any exception (except for client ones)
:param logger: class logger
:return: built middleware
- '''
+ """
@middleware
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
try:
@@ -40,7 +40,7 @@ def exception_handler(logger: Logger) -> Callable[[Request, HandlerType], Awaita
except HTTPClientError:
raise
except Exception:
- logger.exception(f'exception during performing request to {request.path}', exc_info=True)
+ logger.exception(f"exception during performing request to {request.path}")
raise
return handle
diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py
index f7f63e10..48ef29a9 100644
--- a/src/ahriman/web/routes.py
+++ b/src/ahriman/web/routes.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -26,7 +26,7 @@ from ahriman.web.views.packages import PackagesView
def setup_routes(application: Application) -> None:
- '''
+ """
setup all defined routes
Available routes are:
@@ -45,16 +45,16 @@ def setup_routes(application: Application) -> None:
POST /api/v1/package/:base update package base status
:param application: web application instance
- '''
- application.router.add_get('/', IndexView)
- application.router.add_get('/index.html', IndexView)
+ """
+ application.router.add_get("/", IndexView)
+ application.router.add_get("/index.html", IndexView)
- application.router.add_get('/api/v1/ahriman', AhrimanView)
- application.router.add_post('/api/v1/ahriman', AhrimanView)
+ application.router.add_get("/api/v1/ahriman", AhrimanView)
+ application.router.add_post("/api/v1/ahriman", AhrimanView)
- application.router.add_get('/api/v1/packages', PackagesView)
- application.router.add_post('/api/v1/packages', PackagesView)
+ application.router.add_get("/api/v1/packages", PackagesView)
+ application.router.add_post("/api/v1/packages", PackagesView)
- application.router.add_delete('/api/v1/packages/{package}', PackageView)
- application.router.add_get('/api/v1/packages/{package}', PackageView)
- application.router.add_post('/api/v1/packages/{package}', PackageView)
+ application.router.add_delete("/api/v1/packages/{package}", PackageView)
+ application.router.add_get("/api/v1/packages/{package}", PackageView)
+ application.router.add_post("/api/v1/packages/{package}", PackageView)
diff --git a/src/ahriman/web/views/__init__.py b/src/ahriman/web/views/__init__.py
index b7917f9a..fb32931e 100644
--- a/src/ahriman/web/views/__init__.py
+++ b/src/ahriman/web/views/__init__.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
diff --git a/src/ahriman/web/views/ahriman.py b/src/ahriman/web/views/ahriman.py
index 1fb077fb..83b5fe62 100644
--- a/src/ahriman/web/views/ahriman.py
+++ b/src/ahriman/web/views/ahriman.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -24,19 +24,19 @@ from ahriman.web.views.base import BaseView
class AhrimanView(BaseView):
- '''
+ """
service status web view
- '''
+ """
async def get(self) -> Response:
- '''
+ """
get current service status
:return: 200 with service status object
- '''
+ """
return json_response(self.service.status.view())
async def post(self) -> Response:
- '''
+ """
update service status
JSON body must be supplied, the following model is used:
@@ -45,11 +45,11 @@ class AhrimanView(BaseView):
}
:return: 204 on success
- '''
+ """
data = await self.request.json()
try:
- status = BuildStatusEnum(data['status'])
+ status = BuildStatusEnum(data["status"])
except Exception as e:
raise HTTPBadRequest(text=str(e))
diff --git a/src/ahriman/web/views/base.py b/src/ahriman/web/views/base.py
index e5f31ab3..0110cf7f 100644
--- a/src/ahriman/web/views/base.py
+++ b/src/ahriman/web/views/base.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -19,18 +19,18 @@
#
from aiohttp.web import View
-from ahriman.core.watcher.watcher import Watcher
+from ahriman.core.status.watcher import Watcher
class BaseView(View):
- '''
+ """
base web view to make things typed
- '''
+ """
@property
def service(self) -> Watcher:
- '''
+ """
:return: build status watcher instance
- '''
- watcher: Watcher = self.request.app['watcher']
+ """
+ watcher: Watcher = self.request.app["watcher"]
return watcher
diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py
index bd4c2dc2..270a983d 100644
--- a/src/ahriman/web/views/index.py
+++ b/src/ahriman/web/views/index.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -28,7 +28,7 @@ from ahriman.web.views.base import BaseView
class IndexView(BaseView):
- '''
+ """
root view
It uses jinja2 templates for report generation, the following variables are allowed:
@@ -39,35 +39,35 @@ class IndexView(BaseView):
repository - repository name, string, required
service - service status properties: status, status_color, timestamp. Required
version - ahriman version, string, required
- '''
+ """
- @aiohttp_jinja2.template('build-status.jinja2')
+ @aiohttp_jinja2.template("build-status.jinja2")
async def get(self) -> Dict[str, Any]:
- '''
+ """
process get request. No parameters supported here
:return: parameters for jinja template
- '''
+ """
# some magic to make it jinja-friendly
packages = [
{
- 'base': package.base,
- 'packages': list(sorted(package.packages)),
- 'status': status.status.value,
- 'timestamp': pretty_datetime(status.timestamp),
- 'version': package.version,
- 'web_url': package.web_url
+ "base": package.base,
+ "packages": list(sorted(package.packages)),
+ "status": status.status.value,
+ "timestamp": pretty_datetime(status.timestamp),
+ "version": package.version,
+ "web_url": package.web_url
} for package, status in sorted(self.service.packages, key=lambda item: item[0].base)
]
service = {
- 'status': self.service.status.status.value,
- 'status_color': self.service.status.status.badges_color(),
- 'timestamp': pretty_datetime(self.service.status.timestamp)
+ "status": self.service.status.status.value,
+ "status_color": self.service.status.status.badges_color(),
+ "timestamp": pretty_datetime(self.service.status.timestamp)
}
return {
- 'architecture': self.service.architecture,
- 'packages': packages,
- 'repository': self.service.repository.name,
- 'service': service,
- 'version': version.__version__,
+ "architecture": self.service.architecture,
+ "packages": packages,
+ "repository": self.service.repository.name,
+ "service": service,
+ "version": version.__version__,
}
diff --git a/src/ahriman/web/views/package.py b/src/ahriman/web/views/package.py
index 1236383d..97de0dff 100644
--- a/src/ahriman/web/views/package.py
+++ b/src/ahriman/web/views/package.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -19,48 +19,49 @@
#
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
+from ahriman.core.exceptions import UnknownPackage
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.web.views.base import BaseView
class PackageView(BaseView):
- '''
+ """
package base specific web view
- '''
+ """
async def get(self) -> Response:
- '''
+ """
get current package base status
:return: 200 with package description on success
- '''
- base = self.request.match_info['package']
+ """
+ base = self.request.match_info["package"]
try:
package, status = self.service.get(base)
- except KeyError:
+ except UnknownPackage:
raise HTTPNotFound()
response = [
{
- 'package': package.view(),
- 'status': status.view()
+ "package": package.view(),
+ "status": status.view()
}
]
return json_response(response)
async def delete(self) -> Response:
- '''
+ """
delete package base from status page
:return: 204 on success
- '''
- base = self.request.match_info['package']
+ """
+ base = self.request.match_info["package"]
self.service.remove(base)
return HTTPNoContent()
async def post(self) -> Response:
- '''
+ """
update package build status
JSON body must be supplied, the following model is used:
@@ -71,19 +72,19 @@ class PackageView(BaseView):
}
:return: 204 on success
- '''
- base = self.request.match_info['package']
+ """
+ base = self.request.match_info["package"]
data = await self.request.json()
try:
- package = Package.from_json(data['package']) if 'package' in data else None
- status = BuildStatusEnum(data['status'])
+ package = Package.from_json(data["package"]) if "package" in data else None
+ status = BuildStatusEnum(data["status"])
except Exception as e:
raise HTTPBadRequest(text=str(e))
try:
self.service.update(base, status, package)
- except KeyError:
- raise HTTPBadRequest(text=f'Package {base} is unknown, but no package body set')
+ except UnknownPackage:
+ raise HTTPBadRequest(text=f"Package {base} is unknown, but no package body set")
return HTTPNoContent()
diff --git a/src/ahriman/web/views/packages.py b/src/ahriman/web/views/packages.py
index 2d21c688..bd9ed319 100644
--- a/src/ahriman/web/views/packages.py
+++ b/src/ahriman/web/views/packages.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -23,28 +23,28 @@ from ahriman.web.views.base import BaseView
class PackagesView(BaseView):
- '''
+ """
global watcher view
- '''
+ """
async def get(self) -> Response:
- '''
+ """
get current packages status
:return: 200 with package description on success
- '''
+ """
response = [
{
- 'package': package.view(),
- 'status': status.view()
+ "package": package.view(),
+ "status": status.view()
} for package, status in self.service.packages
]
return json_response(response)
async def post(self) -> Response:
- '''
+ """
reload all packages from repository. No parameters supported here
:return: 204 on success
- '''
+ """
self.service.load()
return HTTPNoContent()
diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py
index 354c92b9..888fa3c4 100644
--- a/src/ahriman/web/web.py
+++ b/src/ahriman/web/web.py
@@ -1,5 +1,5 @@
#
-# Copyright (c) 2021 Evgenii Alekseev.
+# Copyright (c) 2021 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
@@ -25,71 +25,72 @@ from aiohttp import web
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import InitializeException
-from ahriman.core.watcher.watcher import Watcher
+from ahriman.core.status.watcher import Watcher
from ahriman.web.middlewares.exception_handler import exception_handler
from ahriman.web.routes import setup_routes
async def on_shutdown(application: web.Application) -> None:
- '''
+ """
web application shutdown handler
:param application: web application instance
- '''
- application.logger.warning('server terminated')
+ """
+ application.logger.warning("server terminated")
async def on_startup(application: web.Application) -> None:
- '''
+ """
web application start handler
:param application: web application instance
- '''
- application.logger.info('server started')
+ """
+ application.logger.info("server started")
try:
- application['watcher'].load()
+ application["watcher"].load()
except Exception:
- application.logger.exception('could not load packages', exc_info=True)
+ application.logger.exception("could not load packages")
raise InitializeException()
-def run_server(application: web.Application, architecture: str) -> None:
- '''
+def run_server(application: web.Application) -> None:
+ """
run web application
:param application: web application instance
- :param architecture: repository architecture
- '''
- application.logger.info('start server')
+ """
+ application.logger.info("start server")
- section = application['config'].get_section_name('web', architecture)
- host = application['config'].get(section, 'host')
- port = application['config'].getint(section, 'port')
+ section = application["config"].get_section_name("web", application["architecture"])
+ host = application["config"].get(section, "host")
+ port = application["config"].getint(section, "port")
web.run_app(application, host=host, port=port, handle_signals=False,
- access_log=logging.getLogger('http'))
+ access_log=logging.getLogger("http"))
def setup_service(architecture: str, config: Configuration) -> web.Application:
- '''
+ """
create web application
:param architecture: repository architecture
:param config: configuration instance
:return: web application instance
- '''
- application = web.Application(logger=logging.getLogger('http'))
+ """
+ application = web.Application(logger=logging.getLogger("http"))
application.on_shutdown.append(on_shutdown)
application.on_startup.append(on_startup)
application.middlewares.append(web.normalize_path_middleware(append_slash=False, remove_slash=True))
application.middlewares.append(exception_handler(application.logger))
- application.logger.info('setup routes')
+ application.logger.info("setup routes")
setup_routes(application)
- application.logger.info('setup templates')
- aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.get('web', 'templates')))
- application.logger.info('setup configuration')
- application['config'] = config
+ application.logger.info("setup templates")
+ aiohttp_jinja2.setup(application, loader=jinja2.FileSystemLoader(config.getpath("web", "templates")))
- application.logger.info('setup watcher')
- application['watcher'] = Watcher(architecture, config)
+ application.logger.info("setup configuration")
+ application["config"] = config
+ application["architecture"] = architecture
+
+ application.logger.info("setup watcher")
+ application["watcher"] = Watcher(architecture, config)
return application
diff --git a/tests/ahriman/application/conftest.py b/tests/ahriman/application/conftest.py
new file mode 100644
index 00000000..67ee365c
--- /dev/null
+++ b/tests/ahriman/application/conftest.py
@@ -0,0 +1,30 @@
+import argparse
+import pytest
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.ahriman import _parser
+from ahriman.application.application import Application
+from ahriman.application.lock import Lock
+from ahriman.core.configuration import Configuration
+
+
+@pytest.fixture
+def application(configuration: Configuration, mocker: MockerFixture) -> Application:
+ mocker.patch("pathlib.Path.mkdir")
+ return Application("x86_64", configuration)
+
+
+@pytest.fixture
+def args() -> argparse.Namespace:
+ return argparse.Namespace(lock=None, force=False, unsafe=False, no_report=True)
+
+
+@pytest.fixture
+def lock(args: argparse.Namespace, configuration: Configuration) -> Lock:
+ return Lock(args, "x86_64", configuration)
+
+
+@pytest.fixture
+def parser() -> argparse.ArgumentParser:
+ return _parser()
diff --git a/tests/ahriman/application/handlers/test_handler.py b/tests/ahriman/application/handlers/test_handler.py
new file mode 100644
index 00000000..3a07dc38
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler.py
@@ -0,0 +1,27 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Handler
+from ahriman.core.configuration import Configuration
+
+
+def test_call(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must call inside lock
+ """
+ mocker.patch("ahriman.application.handlers.Handler.run")
+ enter_mock = mocker.patch("ahriman.application.lock.Lock.__enter__")
+ exit_mock = mocker.patch("ahriman.application.lock.Lock.__exit__")
+
+ assert Handler._call(args, "x86_64", configuration)
+ enter_mock.assert_called_once()
+ exit_mock.assert_called_once()
+
+
+def test_call_exception(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must process exception
+ """
+ mocker.patch("ahriman.application.lock.Lock.__enter__", side_effect=Exception())
+ assert not Handler._call(args, "x86_64", configuration)
diff --git a/tests/ahriman/application/handlers/test_handler_add.py b/tests/ahriman/application/handlers/test_handler_add.py
new file mode 100644
index 00000000..671ff6ee
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_add.py
@@ -0,0 +1,19 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Add
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.package = []
+ args.without_dependencies = False
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.application.application.Application.add")
+
+ Add.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_clean.py b/tests/ahriman/application/handlers/test_handler_clean.py
new file mode 100644
index 00000000..50555798
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_clean.py
@@ -0,0 +1,22 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Clean
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.no_build = False
+ args.no_cache = False
+ args.no_chroot = False
+ args.no_manual = False
+ args.no_packages = False
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.application.application.Application.clean")
+
+ Clean.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_dump.py b/tests/ahriman/application/handlers/test_handler_dump.py
new file mode 100644
index 00000000..c2ae9434
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_dump.py
@@ -0,0 +1,17 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Dump
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump")
+
+ Dump.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_rebuild.py b/tests/ahriman/application/handlers/test_handler_rebuild.py
new file mode 100644
index 00000000..173e09b6
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_rebuild.py
@@ -0,0 +1,19 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Rebuild
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ mocker.patch("pathlib.Path.mkdir")
+ application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages")
+ application_mock = mocker.patch("ahriman.application.application.Application.update")
+
+ Rebuild.run(args, "x86_64", configuration)
+ application_packages_mock.assert_called_once()
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_remove.py b/tests/ahriman/application/handlers/test_handler_remove.py
new file mode 100644
index 00000000..5f9a22b2
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_remove.py
@@ -0,0 +1,18 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Remove
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.package = []
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.application.application.Application.remove")
+
+ Remove.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_report.py b/tests/ahriman/application/handlers/test_handler_report.py
new file mode 100644
index 00000000..9dbba316
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_report.py
@@ -0,0 +1,18 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Report
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.target = []
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.application.application.Application.report")
+
+ Report.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_status.py b/tests/ahriman/application/handlers/test_handler_status.py
new file mode 100644
index 00000000..8ded654e
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_status.py
@@ -0,0 +1,22 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Status
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.ahriman = True
+ args.package = []
+ args.without_dependencies = False
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.core.status.client.Client.get_self")
+ packages_mock = mocker.patch("ahriman.core.status.client.Client.get")
+
+ Status.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
+ packages_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_sync.py b/tests/ahriman/application/handlers/test_handler_sync.py
new file mode 100644
index 00000000..4926976e
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_sync.py
@@ -0,0 +1,18 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Sync
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.target = []
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.application.application.Application.sync")
+
+ Sync.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_update.py b/tests/ahriman/application/handlers/test_handler_update.py
new file mode 100644
index 00000000..3de20521
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_update.py
@@ -0,0 +1,40 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Update
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ args.package = []
+ args.dry_run = False
+ args.no_aur = False
+ args.no_manual = False
+ args.no_vcs = False
+ mocker.patch("pathlib.Path.mkdir")
+ application_mock = mocker.patch("ahriman.application.application.Application.update")
+ updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
+
+ Update.run(args, "x86_64", configuration)
+ application_mock.assert_called_once()
+ updates_mock.assert_called_once()
+
+
+def test_run_dry_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run simplified command
+ """
+ args.package = []
+ args.dry_run = True
+ args.no_aur = False
+ args.no_manual = False
+ args.no_vcs = False
+ mocker.patch("pathlib.Path.mkdir")
+ updates_mock = mocker.patch("ahriman.application.application.Application.get_updates")
+
+ Update.run(args, "x86_64", configuration)
+ updates_mock.assert_called_once()
diff --git a/tests/ahriman/application/handlers/test_handler_web.py b/tests/ahriman/application/handlers/test_handler_web.py
new file mode 100644
index 00000000..62f45f97
--- /dev/null
+++ b/tests/ahriman/application/handlers/test_handler_web.py
@@ -0,0 +1,19 @@
+import argparse
+
+from pytest_mock import MockerFixture
+
+from ahriman.application.handlers import Web
+from ahriman.core.configuration import Configuration
+
+
+def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must run command
+ """
+ mocker.patch("pathlib.Path.mkdir")
+ setup_mock = mocker.patch("ahriman.web.web.setup_service")
+ run_mock = mocker.patch("ahriman.web.web.run_server")
+
+ Web.run(args, "x86_64", configuration)
+ setup_mock.assert_called_once()
+ run_mock.assert_called_once()
diff --git a/tests/ahriman/application/test_ahriman.py b/tests/ahriman/application/test_ahriman.py
new file mode 100644
index 00000000..ea87dd59
--- /dev/null
+++ b/tests/ahriman/application/test_ahriman.py
@@ -0,0 +1,55 @@
+import argparse
+
+
+def test_parser(parser: argparse.ArgumentParser) -> None:
+ """
+ must parse valid command line
+ """
+ parser.parse_args(["-a", "x86_64", "config"])
+
+
+def test_multiple_architectures(parser: argparse.ArgumentParser) -> None:
+ """
+ must accept multiple architectures
+ """
+ args = parser.parse_args(["-a", "x86_64", "-a", "i686", "config"])
+ assert len(args.architecture) == 2
+
+
+def test_subparsers_check(parser: argparse.ArgumentParser) -> None:
+ """
+ check command must imply no_aur, no_manual and dry_run
+ """
+ args = parser.parse_args(["-a", "x86_64", "check"])
+ assert not args.no_aur
+ assert args.no_manual
+ assert args.dry_run
+
+
+def test_subparsers_config(parser: argparse.ArgumentParser) -> None:
+ """
+ config command must imply lock, no_report and unsafe
+ """
+ args = parser.parse_args(["-a", "x86_64", "config"])
+ assert args.lock is None
+ assert args.no_report
+ assert args.unsafe
+
+
+def test_subparsers_status(parser: argparse.ArgumentParser) -> None:
+ """
+ status command must imply lock, no_report and unsafe
+ """
+ args = parser.parse_args(["-a", "x86_64", "status"])
+ assert args.lock is None
+ assert args.no_report
+ assert args.unsafe
+
+
+def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
+ """
+ web command must imply lock and no_report
+ """
+ args = parser.parse_args(["-a", "x86_64", "web"])
+ assert args.lock is None
+ assert args.no_report
diff --git a/tests/ahriman/application/test_application.py b/tests/ahriman/application/test_application.py
new file mode 100644
index 00000000..752538de
--- /dev/null
+++ b/tests/ahriman/application/test_application.py
@@ -0,0 +1,237 @@
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.application.application import Application
+from ahriman.core.tree import Leaf, Tree
+from ahriman.models.package import Package
+
+
+def test_finalize(application: Application, mocker: MockerFixture) -> None:
+ """
+ must report and sync at the last
+ """
+ report_mock = mocker.patch("ahriman.application.application.Application.report")
+ sync_mock = mocker.patch("ahriman.application.application.Application.sync")
+
+ application._finalize()
+ report_mock.assert_called_once()
+ sync_mock.assert_called_once()
+
+
+def test_get_updates_all(application: Application, mocker: MockerFixture) -> None:
+ """
+ must get updates for all
+ """
+ updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
+ updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
+
+ application.get_updates([], no_aur=False, no_manual=False, no_vcs=False, log_fn=print)
+ updates_aur_mock.assert_called_with([], False)
+ updates_manual_mock.assert_called_once()
+
+
+def test_get_updates_disabled(application: Application, mocker: MockerFixture) -> None:
+ """
+ must get updates without anything
+ """
+ updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
+ updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
+
+ application.get_updates([], no_aur=True, no_manual=True, no_vcs=False, log_fn=print)
+ updates_aur_mock.assert_not_called()
+ updates_manual_mock.assert_not_called()
+
+
+def test_get_updates_no_aur(application: Application, mocker: MockerFixture) -> None:
+ """
+ must get updates without aur
+ """
+ updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
+ updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
+
+ application.get_updates([], no_aur=True, no_manual=False, no_vcs=False, log_fn=print)
+ updates_aur_mock.assert_not_called()
+ updates_manual_mock.assert_called_once()
+
+
+def test_get_updates_no_manual(application: Application, mocker: MockerFixture) -> None:
+ """
+ must get updates without manual
+ """
+ updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
+ updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
+
+ application.get_updates([], no_aur=False, no_manual=True, no_vcs=False, log_fn=print)
+ updates_aur_mock.assert_called_with([], False)
+ updates_manual_mock.assert_not_called()
+
+
+def test_get_updates_no_vcs(application: Application, mocker: MockerFixture) -> None:
+ """
+ must get updates without VCS
+ """
+ updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
+ updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
+
+ application.get_updates([], no_aur=False, no_manual=False, no_vcs=True, log_fn=print)
+ updates_aur_mock.assert_called_with([], True)
+ updates_manual_mock.assert_called_once()
+
+
+def test_get_updates_with_filter(application: Application, mocker: MockerFixture) -> None:
+ """
+ must get updates without VCS
+ """
+ updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
+ updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
+
+ application.get_updates(["filter"], no_aur=False, no_manual=False, no_vcs=False, log_fn=print)
+ updates_aur_mock.assert_called_with(["filter"], False)
+ updates_manual_mock.assert_called_once()
+
+
+def test_add_directory(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must add packages from directory
+ """
+ mocker.patch("ahriman.application.application.Application._known_packages", return_value=set())
+ mocker.patch("pathlib.Path.is_dir", return_value=True)
+ iterdir_mock = mocker.patch("pathlib.Path.iterdir",
+ return_value=[package.filepath for package in package_ahriman.packages.values()])
+ move_mock = mocker.patch("shutil.move")
+
+ application.add([package_ahriman.base], False)
+ iterdir_mock.assert_called_once()
+ move_mock.assert_called_once()
+
+
+def test_add_manual(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must add package from AUR
+ """
+ mocker.patch("ahriman.application.application.Application._known_packages", return_value=set())
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ fetch_mock = mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+
+ application.add([package_ahriman.base], True)
+ fetch_mock.assert_called_once()
+
+
+def test_add_manual_with_dependencies(application: Application, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must add package from AUR with dependencies
+ """
+ mocker.patch("ahriman.application.application.Application._known_packages", return_value=set())
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+ dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies")
+
+ application.add([package_ahriman.base], False)
+ dependencies_mock.assert_called_once()
+
+
+def test_add_package(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must add package from archive
+ """
+ mocker.patch("ahriman.application.application.Application._known_packages", return_value=set())
+ mocker.patch("pathlib.Path.is_file", return_value=True)
+ move_mock = mocker.patch("shutil.move")
+
+ application.add([package_ahriman.base], False)
+ move_mock.assert_called_once()
+
+
+def test_clean_build(application: Application, mocker: MockerFixture) -> None:
+ """
+ must clean build directory
+ """
+ clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
+ application.clean(False, True, True, True, True)
+ clear_mock.assert_called_once()
+
+
+def test_clean_cache(application: Application, mocker: MockerFixture) -> None:
+ """
+ must clean cache directory
+ """
+ clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
+ application.clean(True, False, True, True, True)
+ clear_mock.assert_called_once()
+
+
+def test_clean_chroot(application: Application, mocker: MockerFixture) -> None:
+ """
+ must clean chroot directory
+ """
+ clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
+ application.clean(True, True, False, True, True)
+ clear_mock.assert_called_once()
+
+
+def test_clean_manual(application: Application, mocker: MockerFixture) -> None:
+ """
+ must clean manual directory
+ """
+ clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
+ application.clean(True, True, True, False, True)
+ clear_mock.assert_called_once()
+
+
+def test_clean_packages(application: Application, mocker: MockerFixture) -> None:
+ """
+ must clean packages directory
+ """
+ clear_mock = mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
+ application.clean(True, True, True, True, False)
+ clear_mock.assert_called_once()
+
+
+def test_remove(application: Application, mocker: MockerFixture) -> None:
+ """
+ must remove package
+ """
+ executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
+ finalize_mock = mocker.patch("ahriman.application.application.Application._finalize")
+
+ application.remove([])
+ executor_mock.assert_called_once()
+ finalize_mock.assert_called_once()
+
+
+def test_report(application: Application, mocker: MockerFixture) -> None:
+ """
+ must generate report
+ """
+ executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_report")
+ application.report(None)
+ executor_mock.assert_called_once()
+
+
+def test_sync(application: Application, mocker: MockerFixture) -> None:
+ """
+ must sync to remote
+ """
+ executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_sync")
+ application.sync(None)
+ executor_mock.assert_called_once()
+
+
+def test_update(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must process package updates
+ """
+ paths = [package.filepath for package in package_ahriman.packages.values()]
+ tree = Tree([Leaf(package_ahriman, set())])
+
+ mocker.patch("ahriman.core.tree.Tree.load", return_value=tree)
+ mocker.patch("ahriman.core.repository.repository.Repository.packages_built", return_value=[])
+ build_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_build", return_value=paths)
+ update_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_update")
+ finalize_mock = mocker.patch("ahriman.application.application.Application._finalize")
+
+ application.update([package_ahriman])
+ build_mock.assert_called_once()
+ update_mock.assert_has_calls([mock.call([]), mock.call(paths)])
+ finalize_mock.assert_has_calls([mock.call(), mock.call()])
diff --git a/tests/ahriman/application/test_lock.py b/tests/ahriman/application/test_lock.py
new file mode 100644
index 00000000..c3573558
--- /dev/null
+++ b/tests/ahriman/application/test_lock.py
@@ -0,0 +1,151 @@
+import pytest
+import tempfile
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.application.lock import Lock
+from ahriman.core.exceptions import DuplicateRun, UnsafeRun
+from ahriman.models.build_status import BuildStatusEnum
+
+
+def test_enter(lock: Lock, mocker: MockerFixture) -> None:
+ """
+ must process with context manager
+ """
+ check_user_mock = mocker.patch("ahriman.application.lock.Lock.check_user")
+ remove_mock = mocker.patch("ahriman.application.lock.Lock.remove")
+ create_mock = mocker.patch("ahriman.application.lock.Lock.create")
+ update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
+
+ with lock:
+ pass
+ check_user_mock.assert_called_once()
+ remove_mock.assert_called_once()
+ create_mock.assert_called_once()
+ update_status_mock.assert_has_calls([
+ mock.call(BuildStatusEnum.Building),
+ mock.call(BuildStatusEnum.Success)
+ ])
+
+
+def test_exit_with_exception(lock: Lock, mocker: MockerFixture) -> None:
+ """
+ must process with context manager in case if exception raised
+ """
+ mocker.patch("ahriman.application.lock.Lock.check_user")
+ mocker.patch("ahriman.application.lock.Lock.remove")
+ mocker.patch("ahriman.application.lock.Lock.create")
+ update_status_mock = mocker.patch("ahriman.core.status.client.Client.update_self")
+
+ with pytest.raises(Exception):
+ with lock:
+ raise Exception()
+ update_status_mock.assert_has_calls([
+ mock.call(BuildStatusEnum.Building),
+ mock.call(BuildStatusEnum.Failed)
+ ])
+
+
+def test_check_user(lock: Lock, mocker: MockerFixture) -> None:
+ """
+ must check user correctly
+ """
+ stat = Path.cwd().stat()
+ mocker.patch("pathlib.Path.stat", return_value=stat)
+ mocker.patch("os.getuid", return_value=stat.st_uid)
+
+ lock.check_user()
+
+
+def test_check_user_exception(lock: Lock, mocker: MockerFixture) -> None:
+ """
+ must raise exception if user differs
+ """
+ stat = Path.cwd().stat()
+ mocker.patch("pathlib.Path.stat")
+ mocker.patch("os.getuid", return_value=stat.st_uid + 1)
+
+ with pytest.raises(UnsafeRun):
+ lock.check_user()
+
+
+def test_check_user_unsafe(lock: Lock) -> None:
+ """
+ must skip user check if unsafe flag set
+ """
+ lock.unsafe = True
+ lock.check_user()
+
+
+def test_create(lock: Lock) -> None:
+ """
+ must create lock
+ """
+ lock.path = Path(tempfile.mktemp())
+
+ lock.create()
+ assert lock.path.is_file()
+ lock.path.unlink()
+
+
+def test_create_exception(lock: Lock) -> None:
+ """
+ must raise exception if file already exists
+ """
+ lock.path = Path(tempfile.mktemp())
+ lock.path.touch()
+
+ with pytest.raises(DuplicateRun):
+ lock.create()
+ lock.path.unlink()
+
+
+def test_create_skip(lock: Lock, mocker: MockerFixture) -> None:
+ """
+ must skip creating if no file set
+ """
+ touch_mock = mocker.patch("pathlib.Path.touch")
+ lock.create()
+ touch_mock.assert_not_called()
+
+
+def test_create_unsafe(lock: Lock) -> None:
+ """
+ must not raise exception if force flag set
+ """
+ lock.force = True
+ lock.path = Path(tempfile.mktemp())
+ lock.path.touch()
+
+ lock.create()
+ lock.path.unlink()
+
+
+def test_remove(lock: Lock) -> None:
+ """
+ must remove lock file
+ """
+ lock.path = Path(tempfile.mktemp())
+ lock.path.touch()
+
+ lock.remove()
+ assert not lock.path.is_file()
+
+
+def test_remove_missing(lock: Lock) -> None:
+ """
+ must not fail on lock removal if file is missing
+ """
+ lock.path = Path(tempfile.mktemp())
+ lock.remove()
+
+
+def test_remove_skip(lock: Lock, mocker: MockerFixture) -> None:
+ """
+ must skip removal if no file set
+ """
+ unlink_mock = mocker.patch("pathlib.Path.unlink")
+ lock.remove()
+ unlink_mock.assert_not_called()
diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py
new file mode 100644
index 00000000..ee5aa2d2
--- /dev/null
+++ b/tests/ahriman/conftest.py
@@ -0,0 +1,95 @@
+import pytest
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from typing import Any, Type, TypeVar
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.status.watcher import Watcher
+from ahriman.models.package import Package
+from ahriman.models.package_desciption import PackageDescription
+from ahriman.models.repository_paths import RepositoryPaths
+
+T = TypeVar("T")
+
+
+# helpers
+# https://stackoverflow.com/a/21611963
+@pytest.helpers.register
+def anyvar(cls: Type[T], strict: bool = False) -> T:
+ class AnyVar(cls):
+ def __eq__(self, other: Any) -> bool:
+ return not strict or isinstance(other, cls)
+ return AnyVar()
+
+
+# generic fixtures
+@pytest.fixture
+def configuration(resource_path_root: Path) -> Configuration:
+ path = resource_path_root / "core" / "ahriman.ini"
+ return Configuration.from_path(path=path, logfile=False)
+
+
+@pytest.fixture
+def package_ahriman(package_description_ahriman: PackageDescription) -> Package:
+ packages = {"ahriman": package_description_ahriman}
+ return Package(
+ base="ahriman",
+ version="0.12.1-1",
+ aur_url="https://aur.archlinux.org",
+ packages=packages)
+
+
+@pytest.fixture
+def package_python_schedule(
+ package_description_python_schedule: PackageDescription,
+ package_description_python2_schedule: PackageDescription) -> Package:
+ packages = {
+ "python-schedule": package_description_python_schedule,
+ "python2-schedule": package_description_python2_schedule
+ }
+ return Package(
+ base="python-schedule",
+ version="1.0.0-2",
+ aur_url="https://aur.archlinux.org",
+ packages=packages)
+
+
+@pytest.fixture
+def package_description_ahriman() -> PackageDescription:
+ return PackageDescription(
+ archive_size=4200,
+ build_date=42,
+ filename="ahriman-0.12.1-1-any.pkg.tar.zst",
+ installed_size=4200000)
+
+
+@pytest.fixture
+def package_description_python_schedule() -> PackageDescription:
+ return PackageDescription(
+ archive_size=4201,
+ build_date=421,
+ filename="python-schedule-1.0.0-2-any.pkg.tar.zst",
+ installed_size=4200001)
+
+
+@pytest.fixture
+def package_description_python2_schedule() -> PackageDescription:
+ return PackageDescription(
+ archive_size=4202,
+ build_date=422,
+ filename="python2-schedule-1.0.0-2-any.pkg.tar.zst",
+ installed_size=4200002)
+
+
+@pytest.fixture
+def repository_paths(configuration: Configuration) -> RepositoryPaths:
+ return RepositoryPaths(
+ architecture="x86_64",
+ root=configuration.getpath("repository", "root"))
+
+
+@pytest.fixture
+def watcher(configuration: Configuration, mocker: MockerFixture) -> Watcher:
+ mocker.patch("pathlib.Path.mkdir")
+ return Watcher("x86_64", configuration)
diff --git a/tests/ahriman/core/alpm/test_pacman.py b/tests/ahriman/core/alpm/test_pacman.py
new file mode 100644
index 00000000..030fe1a4
--- /dev/null
+++ b/tests/ahriman/core/alpm/test_pacman.py
@@ -0,0 +1,10 @@
+from ahriman.core.alpm.pacman import Pacman
+
+
+def test_all_packages(pacman: Pacman) -> None:
+ """
+ package list must not be empty
+ """
+ packages = pacman.all_packages()
+ assert packages
+ assert "pacman" in packages
diff --git a/tests/ahriman/core/alpm/test_repo.py b/tests/ahriman/core/alpm/test_repo.py
new file mode 100644
index 00000000..e2b7a48f
--- /dev/null
+++ b/tests/ahriman/core/alpm/test_repo.py
@@ -0,0 +1,47 @@
+import pytest
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+
+from ahriman.core.alpm.repo import Repo
+
+
+def test_repo_path(repo: Repo) -> None:
+ """
+ name must be something like archive name
+ """
+ assert repo.repo_path.name.endswith("db.tar.gz")
+
+
+def test_repo_add(repo: Repo, mocker: MockerFixture) -> None:
+ """
+ must call repo-add on package addition
+ """
+ check_output_mock = mocker.patch("ahriman.core.alpm.repo.Repo._check_output")
+
+ repo.add(Path("path"))
+ check_output_mock.assert_called_once()
+ assert check_output_mock.call_args[0][0] == "repo-add"
+
+
+def test_repo_remove(repo: Repo, mocker: MockerFixture) -> None:
+ """
+ must call repo-remove on package addition
+ """
+ mocker.patch("pathlib.Path.glob", return_value=[])
+ check_output_mock = mocker.patch("ahriman.core.alpm.repo.Repo._check_output")
+
+ repo.remove("package", Path("package.pkg.tar.xz"))
+ check_output_mock.assert_called_once()
+ assert check_output_mock.call_args[0][0] == "repo-remove"
+
+
+def test_repo_remove_fail_no_file(repo: Repo, mocker: MockerFixture) -> None:
+ """
+ must fail on missing file
+ """
+ mocker.patch("pathlib.Path.glob", return_value=[Path("package.pkg.tar.xz")])
+ mocker.patch("pathlib.Path.unlink", side_effect=FileNotFoundError())
+
+ with pytest.raises(FileNotFoundError):
+ repo.remove("package", Path("package.pkg.tar.xz"))
diff --git a/tests/ahriman/core/build_tools/test_task.py b/tests/ahriman/core/build_tools/test_task.py
new file mode 100644
index 00000000..42b30fb4
--- /dev/null
+++ b/tests/ahriman/core/build_tools/test_task.py
@@ -0,0 +1,64 @@
+import pytest
+import shutil
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.core.build_tools.task import Task
+
+
+def test_fetch_existing(mocker: MockerFixture) -> None:
+ """
+ must fetch new package via clone command
+ """
+ mocker.patch("pathlib.Path.is_dir", return_value=True)
+ check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
+
+ local = Path("local")
+ Task.fetch(local, "remote", "master")
+ check_output_mock.assert_has_calls([
+ mock.call("git", "fetch", "origin", "master",
+ exception=pytest.helpers.anyvar(int),
+ cwd=local, logger=pytest.helpers.anyvar(int)),
+ mock.call("git", "checkout", "--force", "master",
+ exception=pytest.helpers.anyvar(int),
+ cwd=local, logger=pytest.helpers.anyvar(int)),
+ mock.call("git", "reset", "--hard", "origin/master",
+ exception=pytest.helpers.anyvar(int),
+ cwd=local, logger=pytest.helpers.anyvar(int))
+ ])
+
+
+def test_fetch_new(mocker: MockerFixture) -> None:
+ """
+ must fetch new package via clone command
+ """
+ mocker.patch("pathlib.Path.is_dir", return_value=False)
+ check_output_mock = mocker.patch("ahriman.core.build_tools.task.Task._check_output")
+
+ local = Path("local")
+ Task.fetch(local, "remote", "master")
+ check_output_mock.assert_has_calls([
+ mock.call("git", "clone", "remote", str(local),
+ exception=pytest.helpers.anyvar(int),
+ logger=pytest.helpers.anyvar(int)),
+ mock.call("git", "checkout", "--force", "master",
+ exception=pytest.helpers.anyvar(int),
+ cwd=local, logger=pytest.helpers.anyvar(int)),
+ mock.call("git", "reset", "--hard", "origin/master",
+ exception=pytest.helpers.anyvar(int),
+ cwd=local, logger=pytest.helpers.anyvar(int))
+ ])
+
+
+def test_init_with_cache(task_ahriman: Task, mocker: MockerFixture) -> None:
+ """
+ must copy tree instead of fetch
+ """
+ mocker.patch("pathlib.Path.is_dir", return_value=True)
+ mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+ copytree_mock = mocker.patch("shutil.copytree")
+
+ task_ahriman.init(None)
+ copytree_mock.assert_called_once()
diff --git a/tests/ahriman/core/conftest.py b/tests/ahriman/core/conftest.py
new file mode 100644
index 00000000..a6f6fc4b
--- /dev/null
+++ b/tests/ahriman/core/conftest.py
@@ -0,0 +1,34 @@
+import pytest
+
+from ahriman.core.alpm.pacman import Pacman
+from ahriman.core.alpm.repo import Repo
+from ahriman.core.build_tools.task import Task
+from ahriman.core.configuration import Configuration
+from ahriman.core.tree import Leaf
+from ahriman.models.package import Package
+from ahriman.models.repository_paths import RepositoryPaths
+
+
+@pytest.fixture
+def leaf_ahriman(package_ahriman: Package) -> Leaf:
+ return Leaf(package_ahriman, set())
+
+
+@pytest.fixture
+def leaf_python_schedule(package_python_schedule: Package) -> Leaf:
+ return Leaf(package_python_schedule, set())
+
+
+@pytest.fixture
+def pacman(configuration: Configuration) -> Pacman:
+ return Pacman(configuration)
+
+
+@pytest.fixture
+def repo(configuration: Configuration, repository_paths: RepositoryPaths) -> Repo:
+ return Repo(configuration.get("repository", "name"), repository_paths, [])
+
+
+@pytest.fixture
+def task_ahriman(package_ahriman: Package, configuration: Configuration, repository_paths: RepositoryPaths) -> Task:
+ return Task(package_ahriman, "x86_64", configuration, repository_paths)
diff --git a/tests/ahriman/core/report/test_html.py b/tests/ahriman/core/report/test_html.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/core/report/test_report.py b/tests/ahriman/core/report/test_report.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/core/repository/conftest.py b/tests/ahriman/core/repository/conftest.py
new file mode 100644
index 00000000..4a8e8e22
--- /dev/null
+++ b/tests/ahriman/core/repository/conftest.py
@@ -0,0 +1,49 @@
+import pytest
+
+from pytest_mock import MockerFixture
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.repository.cleaner import Cleaner
+from ahriman.core.repository.executor import Executor
+from ahriman.core.repository.properties import Properties
+from ahriman.core.repository.repository import Repository
+from ahriman.core.repository.update_handler import UpdateHandler
+
+
+@pytest.fixture
+def cleaner(configuration: Configuration, mocker: MockerFixture) -> Cleaner:
+ mocker.patch("pathlib.Path.mkdir")
+ return Cleaner("x86_64", configuration)
+
+
+@pytest.fixture
+def executor(configuration: Configuration, mocker: MockerFixture) -> Executor:
+ mocker.patch("pathlib.Path.mkdir")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
+ return Executor("x86_64", configuration)
+
+
+@pytest.fixture
+def repository(configuration: Configuration, mocker: MockerFixture) -> Repository:
+ mocker.patch("pathlib.Path.mkdir")
+ return Repository("x86_64", configuration)
+
+
+@pytest.fixture
+def properties(configuration: Configuration) -> Properties:
+ return Properties("x86_64", configuration)
+
+
+@pytest.fixture
+def update_handler(configuration: Configuration, mocker: MockerFixture) -> UpdateHandler:
+ mocker.patch("pathlib.Path.mkdir")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_build")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_cache")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_chroot")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_manual")
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.clear_packages")
+ return UpdateHandler("x86_64", configuration)
diff --git a/tests/ahriman/core/repository/test_cleaner.py b/tests/ahriman/core/repository/test_cleaner.py
new file mode 100644
index 00000000..052644b7
--- /dev/null
+++ b/tests/ahriman/core/repository/test_cleaner.py
@@ -0,0 +1,68 @@
+import shutil
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.core.repository.cleaner import Cleaner
+
+
+def _mock_clear(mocker: MockerFixture) -> None:
+ mocker.patch("pathlib.Path.iterdir", return_value=[Path("a"), Path("b"), Path("c")])
+ mocker.patch("shutil.rmtree")
+
+
+def _mock_clear_check() -> None:
+ shutil.rmtree.assert_has_calls([
+ mock.call(Path("a")),
+ mock.call(Path("b")),
+ mock.call(Path("c"))
+ ])
+
+
+def test_clear_build(cleaner: Cleaner, mocker: MockerFixture) -> None:
+ """
+ must remove directories with sources
+ """
+ _mock_clear(mocker)
+ cleaner.clear_build()
+ _mock_clear_check()
+
+
+def test_clear_cache(cleaner: Cleaner, mocker: MockerFixture) -> None:
+ """
+ must remove every cached sources
+ """
+ _mock_clear(mocker)
+ cleaner.clear_cache()
+ _mock_clear_check()
+
+
+def test_clear_chroot(cleaner: Cleaner, mocker: MockerFixture) -> None:
+ """
+ must clear chroot
+ """
+ _mock_clear(mocker)
+ cleaner.clear_chroot()
+ _mock_clear_check()
+
+
+def test_clear_manual(cleaner: Cleaner, mocker: MockerFixture) -> None:
+ """
+ must clear directory with manual packages
+ """
+ _mock_clear(mocker)
+ cleaner.clear_manual()
+ _mock_clear_check()
+
+
+def test_clear_packages(cleaner: Cleaner, mocker: MockerFixture) -> None:
+ """
+ must delete built packages
+ """
+ mocker.patch("ahriman.core.repository.cleaner.Cleaner.packages_built",
+ return_value=[Path("a"), Path("b"), Path("c")])
+ mocker.patch("pathlib.Path.unlink")
+
+ cleaner.clear_packages()
+ Path.unlink.assert_has_calls([mock.call(), mock.call(), mock.call()])
diff --git a/tests/ahriman/core/repository/test_executor.py b/tests/ahriman/core/repository/test_executor.py
new file mode 100644
index 00000000..524bd58d
--- /dev/null
+++ b/tests/ahriman/core/repository/test_executor.py
@@ -0,0 +1,188 @@
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.core.repository.executor import Executor
+from ahriman.models.package import Package
+
+
+def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must run build process
+ """
+ mocker.patch("ahriman.core.repository.executor.Executor.packages_built", return_value=[package_ahriman])
+ mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
+ mocker.patch("ahriman.core.build_tools.task.Task.init")
+ move_mock = mocker.patch("shutil.move")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
+
+ # must return list of built packages
+ assert executor.process_build([package_ahriman]) == [package_ahriman]
+ # must move files (once)
+ move_mock.assert_called_once()
+ # must update status
+ status_client_mock.assert_called_once()
+ # must clear directory
+ from ahriman.core.repository.cleaner import Cleaner
+ Cleaner.clear_build.assert_called_once()
+
+
+def test_process_build_failure(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must run correct process failed builds
+ """
+ mocker.patch("ahriman.core.repository.executor.Executor.packages_built")
+ mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
+ mocker.patch("ahriman.core.build_tools.task.Task.init")
+ mocker.patch("shutil.move", side_effect=Exception())
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
+
+ executor.process_build([package_ahriman])
+ status_client_mock.assert_called_once()
+
+
+def test_process_remove_base(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must run remove process for whole base
+ """
+ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
+ repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+
+ executor.process_remove([package_ahriman.base])
+ # must remove via alpm wrapper
+ repo_remove_mock.assert_called_once()
+ # must update status
+ status_client_mock.assert_called_once()
+
+
+def test_process_remove_base_multiple(executor: Executor, package_python_schedule: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must run remove process for whole base with multiple packages
+ """
+ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
+ repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+
+ executor.process_remove([package_python_schedule.base])
+ # must remove via alpm wrapper
+ repo_remove_mock.assert_has_calls([
+ mock.call(package, Path(props.filename))
+ for package, props in package_python_schedule.packages.items()
+ ], any_order=True)
+ # must update status
+ status_client_mock.assert_called_once()
+
+
+def test_process_remove_base_single(executor: Executor, package_python_schedule: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must run remove process for single package in base
+ """
+ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_python_schedule])
+ repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.remove")
+
+ executor.process_remove(["python2-schedule"])
+ # must remove via alpm wrapper
+ repo_remove_mock.assert_called_once()
+ # must not update status
+ status_client_mock.assert_not_called()
+
+
+def test_process_remove_nothing(executor: Executor, package_ahriman: Package, package_python_schedule: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must not remove anything if it was not requested
+ """
+ mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
+ repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
+
+ executor.process_remove([package_python_schedule.base])
+ repo_remove_mock.assert_not_called()
+
+
+def test_process_report_auto(executor: Executor, mocker: MockerFixture) -> None:
+ """
+ must process report in auto mode if no targets supplied
+ """
+ config_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
+
+ executor.process_report(None)
+ config_getlist_mock.assert_called_once()
+
+
+def test_process_sync_auto(executor: Executor, mocker: MockerFixture) -> None:
+ """
+ must process sync in auto mode if no targets supplied
+ """
+ config_getlist_mock = mocker.patch("ahriman.core.configuration.Configuration.getlist")
+
+ executor.process_sync(None)
+ config_getlist_mock.assert_called_once()
+
+
+def test_process_update(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must run update process
+ """
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ move_mock = mocker.patch("shutil.move")
+ repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
+ sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.sign_package", side_effect=lambda fn, _: [fn])
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
+
+ # must return complete
+ assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
+ # must move files (once)
+ move_mock.assert_called_once()
+ # must sign package
+ sign_package_mock.assert_called_once()
+ # must add package
+ repo_add_mock.assert_called_once()
+ # must update status
+ status_client_mock.assert_called_once()
+ # must clear directory
+ from ahriman.core.repository.cleaner import Cleaner
+ Cleaner.clear_packages.assert_called_once()
+
+
+def test_process_update_group(executor: Executor, package_python_schedule: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must group single packages under one base
+ """
+ mocker.patch("shutil.move")
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_python_schedule)
+ repo_add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_success")
+
+ executor.process_update([Path(package.filename) for package in package_python_schedule.packages.values()])
+ repo_add_mock.assert_has_calls([
+ mock.call(executor.paths.repository / package.filename)
+ for package in package_python_schedule.packages.values()
+ ], any_order=True)
+ status_client_mock.assert_called_with(package_python_schedule)
+
+
+def test_process_update_failed(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must process update for failed package
+ """
+ mocker.patch("shutil.move", side_effect=Exception())
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
+
+ executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
+ status_client_mock.assert_called_once()
+
+
+def test_process_update_failed_on_load(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must process update even with failed package load
+ """
+ mocker.patch("shutil.move")
+ mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
+
+ assert executor.process_update([Path(package.filename) for package in package_ahriman.packages.values()])
diff --git a/tests/ahriman/core/repository/test_properties.py b/tests/ahriman/core/repository/test_properties.py
new file mode 100644
index 00000000..760176c1
--- /dev/null
+++ b/tests/ahriman/core/repository/test_properties.py
@@ -0,0 +1,14 @@
+from pytest_mock import MockerFixture
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.repository.properties import Properties
+
+
+def test_create_tree_on_load(configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must create tree on load
+ """
+ create_tree_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.create_tree")
+ Properties("x86_64", configuration)
+
+ create_tree_mock.assert_called_once()
diff --git a/tests/ahriman/core/repository/test_repository.py b/tests/ahriman/core/repository/test_repository.py
new file mode 100644
index 00000000..e2d56b2f
--- /dev/null
+++ b/tests/ahriman/core/repository/test_repository.py
@@ -0,0 +1,33 @@
+from pathlib import Path
+from pytest_mock import MockerFixture
+
+from ahriman.core.repository.repository import Repository
+from ahriman.models.package import Package
+
+
+def test_packages(package_ahriman: Package, package_python_schedule: Package,
+ repository: Repository, mocker: MockerFixture) -> None:
+ """
+ must return all packages grouped by package base
+ """
+ single_packages = [
+ Package(base=package_python_schedule.base,
+ version=package_python_schedule.version,
+ aur_url=package_python_schedule.aur_url,
+ packages={package: props})
+ for package, props in package_python_schedule.packages.items()
+ ] + [package_ahriman]
+
+ mocker.patch("pathlib.Path.iterdir",
+ return_value=[Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
+ mocker.patch("ahriman.models.package.Package.load", side_effect=single_packages)
+
+ packages = repository.packages()
+ 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
diff --git a/tests/ahriman/core/repository/test_update_handler.py b/tests/ahriman/core/repository/test_update_handler.py
new file mode 100644
index 00000000..f896a6a0
--- /dev/null
+++ b/tests/ahriman/core/repository/test_update_handler.py
@@ -0,0 +1,124 @@
+from pytest_mock import MockerFixture
+
+from ahriman.core.repository.update_handler import UpdateHandler
+from ahriman.models.package import Package
+
+
+def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must provide updates with status updates
+ """
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
+ mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
+
+ assert update_handler.updates_aur([], False) == [package_ahriman]
+ status_client_mock.assert_called_once()
+
+
+def test_updates_aur_failed(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must update status via client for failed load
+ """
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
+ mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_failed")
+
+ update_handler.updates_aur([], False)
+ status_client_mock.assert_called_once()
+
+
+def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must provide updates only for filtered packages
+ """
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
+ return_value=[package_ahriman, package_python_schedule])
+ mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
+ package_load_mock = mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+
+ assert update_handler.updates_aur([package_ahriman.base], False) == [package_ahriman]
+ package_load_mock.assert_called_once()
+
+
+def test_updates_aur_ignore(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must skip ignore packages
+ """
+ mocker.patch("ahriman.core.configuration.Configuration.getlist", return_value=[package_ahriman.base])
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
+ package_load_mock = mocker.patch("ahriman.models.package.Package.load")
+
+ update_handler.updates_aur([], False)
+ package_load_mock.assert_not_called()
+
+
+def test_updates_aur_ignore_vcs(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must skip VCS packages check if requested
+ """
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
+ mocker.patch("ahriman.models.package.Package.is_vcs", return_value=True)
+ package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated")
+
+ update_handler.updates_aur([], True)
+ package_is_outdated_mock.assert_not_called()
+
+
+def test_updates_manual_clear(update_handler: UpdateHandler, mocker: MockerFixture) -> None:
+ """
+ requesting manual updates must clear packages directory
+ """
+ mocker.patch("pathlib.Path.iterdir", return_value=[])
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages")
+
+ update_handler.updates_manual()
+
+ from ahriman.core.repository.cleaner import Cleaner
+ Cleaner.clear_manual.assert_called_once()
+
+
+def test_updates_manual_status_known(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must create record for known package via reporter
+ """
+ mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
+
+ update_handler.updates_manual()
+ status_client_mock.assert_called_once()
+
+
+def test_updates_manual_status_unknown(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must create record for unknown package via reporter
+ """
+ mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
+ mocker.patch("ahriman.models.package.Package.load", return_value=package_ahriman)
+ status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_unknown")
+
+ update_handler.updates_manual()
+ status_client_mock.assert_called_once()
+
+
+def test_updates_manual_with_failures(update_handler: UpdateHandler, package_ahriman: Package,
+ mocker: MockerFixture) -> None:
+ """
+ must process through the packages with failure
+ """
+ mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman.base])
+ mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[])
+ mocker.patch("ahriman.models.package.Package.load", side_effect=Exception())
+
+ assert update_handler.updates_manual() == []
diff --git a/tests/ahriman/core/sign/conftest.py b/tests/ahriman/core/sign/conftest.py
new file mode 100644
index 00000000..5aa798b7
--- /dev/null
+++ b/tests/ahriman/core/sign/conftest.py
@@ -0,0 +1,9 @@
+import pytest
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.sign.gpg import GPG
+
+
+@pytest.fixture
+def gpg(configuration: Configuration) -> GPG:
+ return GPG("x86_64", configuration)
diff --git a/tests/ahriman/core/sign/gpg.py b/tests/ahriman/core/sign/gpg.py
new file mode 100644
index 00000000..bb8def2c
--- /dev/null
+++ b/tests/ahriman/core/sign/gpg.py
@@ -0,0 +1,61 @@
+from pathlib import Path
+from pytest_mock import MockerFixture
+
+from ahriman.core.sign.gpg import GPG
+from ahriman.models.sign_settings import SignSettings
+
+
+def test_repository_sign_args(gpg: GPG) -> None:
+ """
+ must generate correct sign args
+ """
+ gpg.target = {SignSettings.SignRepository}
+ assert gpg.repository_sign_args
+
+
+def test_sign_package(gpg: GPG, mocker: MockerFixture) -> None:
+ """
+ must sign package
+ """
+ result = [Path("a"), Path("a.sig")]
+ process_mock = mocker.patch("ahriman.core.sign.gpg.process", return_value=result)
+
+ for target in ({SignSettings.SignPackages}, {SignSettings.SignPackages, SignSettings.SignRepository}):
+ gpg.target = target
+ assert gpg.sign_package(Path("a"), "a") == result
+ process_mock.assert_called_once()
+
+
+def test_sign_package_skip(gpg: GPG, mocker: MockerFixture) -> None:
+ """
+ must not sign package if it is not set
+ """
+ process_mock = mocker.patch("ahriman.core.sign.gpg.process")
+
+ for target in ({}, {SignSettings.SignRepository}):
+ gpg.target = target
+ process_mock.assert_not_called()
+
+
+def test_sign_repository(gpg: GPG, mocker: MockerFixture) -> None:
+ """
+ must sign repository
+ """
+ result = [Path("a"), Path("a.sig")]
+ process_mock = mocker.patch("ahriman.core.sign.gpg.process", return_value=result)
+
+ for target in ({SignSettings.SignRepository}, {SignSettings.SignPackages, SignSettings.SignRepository}):
+ gpg.target = target
+ assert gpg.sign_repository(Path("a")) == result
+ process_mock.assert_called_once()
+
+
+def test_sign_repository_skip(gpg: GPG, mocker: MockerFixture) -> None:
+ """
+ must not sign repository if it is not set
+ """
+ process_mock = mocker.patch("ahriman.core.sign.gpg.process")
+
+ for target in ({}, {SignSettings.SignPackages}):
+ gpg.target = target
+ process_mock.assert_not_called()
diff --git a/tests/ahriman/core/status/conftest.py b/tests/ahriman/core/status/conftest.py
new file mode 100644
index 00000000..a3bc3937
--- /dev/null
+++ b/tests/ahriman/core/status/conftest.py
@@ -0,0 +1,30 @@
+import pytest
+
+from typing import Any, Dict
+
+from ahriman.core.status.client import Client
+from ahriman.core.status.web_client import WebClient
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+from ahriman.models.package import Package
+
+
+# helpers
+@pytest.helpers.register
+def get_package_status(package: Package) -> Dict[str, Any]:
+ return {"status": BuildStatusEnum.Unknown.value, "package": package.view()}
+
+
+@pytest.helpers.register
+def get_package_status_extended(package: Package) -> Dict[str, Any]:
+ return {"status": BuildStatus().view(), "package": package.view()}
+
+
+# fixtures
+@pytest.fixture
+def client() -> Client:
+ return Client()
+
+
+@pytest.fixture
+def web_client() -> WebClient:
+ return WebClient("localhost", 8080)
diff --git a/tests/ahriman/core/status/test_client.py b/tests/ahriman/core/status/test_client.py
new file mode 100644
index 00000000..c3c7d2e4
--- /dev/null
+++ b/tests/ahriman/core/status/test_client.py
@@ -0,0 +1,116 @@
+from pytest_mock import MockerFixture
+
+from ahriman.core.configuration import Configuration
+from ahriman.core.status.client import Client
+from ahriman.core.status.web_client import WebClient
+from ahriman.models.build_status import BuildStatusEnum
+from ahriman.models.package import Package
+
+
+def test_add(client: Client, package_ahriman: Package) -> None:
+ """
+ must process package addition without errors
+ """
+ client.add(package_ahriman, BuildStatusEnum.Unknown)
+
+
+def test_get(client: Client, package_ahriman: Package) -> None:
+ """
+ must return empty package list
+ """
+ assert client.get(package_ahriman.base) == []
+ assert client.get(None) == []
+
+
+def test_get_self(client: Client) -> None:
+ """
+ must return unknown status for service
+ """
+ assert client.get_self().status == BuildStatusEnum.Unknown
+
+
+def test_remove(client: Client, package_ahriman: Package) -> None:
+ """
+ must process remove without errors
+ """
+ client.remove(package_ahriman.base)
+
+
+def test_update(client: Client, package_ahriman: Package) -> None:
+ """
+ must update package status without errors
+ """
+ client.update(package_ahriman.base, BuildStatusEnum.Unknown)
+
+
+def test_update_self(client: Client) -> None:
+ """
+ must update self status without errors
+ """
+ client.update_self(BuildStatusEnum.Unknown)
+
+
+def test_set_building(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must set building status to the package
+ """
+ update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ client.set_building(package_ahriman.base)
+
+ update_mock.assert_called_with(package_ahriman.base, BuildStatusEnum.Building)
+
+
+def test_set_failed(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must set failed status to the package
+ """
+ update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ client.set_failed(package_ahriman.base)
+
+ update_mock.assert_called_with(package_ahriman.base, BuildStatusEnum.Failed)
+
+
+def test_set_pending(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must set building status to the package
+ """
+ update_mock = mocker.patch("ahriman.core.status.client.Client.update")
+ client.set_pending(package_ahriman.base)
+
+ update_mock.assert_called_with(package_ahriman.base, BuildStatusEnum.Pending)
+
+
+def test_set_success(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must set success status to the package
+ """
+ add_mock = mocker.patch("ahriman.core.status.client.Client.add")
+ client.set_success(package_ahriman)
+
+ add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Success)
+
+
+def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must add new package with unknown status
+ """
+ add_mock = mocker.patch("ahriman.core.status.client.Client.add")
+ client.set_unknown(package_ahriman)
+
+ add_mock.assert_called_with(package_ahriman, BuildStatusEnum.Unknown)
+
+
+def test_load_dummy_client(configuration: Configuration) -> None:
+ """
+ must load dummy client if no settings set
+ """
+ assert isinstance(Client.load("x86_64", configuration), Client)
+
+
+def test_load_full_client(configuration: Configuration) -> None:
+ """
+ must load full client if no settings set
+ """
+ configuration.set("web", "host", "localhost")
+ configuration.set("web", "port", "8080")
+ assert isinstance(Client.load("x86_64", configuration), WebClient)
diff --git a/tests/ahriman/core/status/test_watcher.py b/tests/ahriman/core/status/test_watcher.py
new file mode 100644
index 00000000..114c2489
--- /dev/null
+++ b/tests/ahriman/core/status/test_watcher.py
@@ -0,0 +1,219 @@
+import pytest
+import tempfile
+
+from pathlib import Path
+from pytest_mock import MockerFixture
+from unittest.mock import PropertyMock
+
+from ahriman.core.exceptions import UnknownPackage
+from ahriman.core.status.watcher import Watcher
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+from ahriman.models.package import Package
+
+
+def test_cache_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must load state from cache
+ """
+ response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
+
+ mocker.patch("pathlib.Path.is_file", return_value=True)
+ mocker.patch("pathlib.Path.open")
+ mocker.patch("json.load", return_value=response)
+
+ watcher.known = {package_ahriman.base: (None, None)}
+ watcher._cache_load()
+
+ package, status = watcher.known[package_ahriman.base]
+ assert package == package_ahriman
+ assert status.status == BuildStatusEnum.Unknown
+
+
+def test_cache_load_json_error(watcher: Watcher, mocker: MockerFixture) -> None:
+ """
+ must not fail on json errors
+ """
+ mocker.patch("pathlib.Path.is_file", return_value=True)
+ mocker.patch("pathlib.Path.open")
+ mocker.patch("json.load", side_effect=Exception())
+
+ watcher._cache_load()
+ assert not watcher.known
+
+
+def test_cache_load_no_file(watcher: Watcher, mocker: MockerFixture) -> None:
+ """
+ must not fail on missing file
+ """
+ mocker.patch("pathlib.Path.is_file", return_value=False)
+
+ watcher._cache_load()
+ assert not watcher.known
+
+
+def test_cache_load_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must not load unknown package
+ """
+ response = {"packages": [pytest.helpers.get_package_status_extended(package_ahriman)]}
+
+ mocker.patch("pathlib.Path.is_file", return_value=True)
+ mocker.patch("pathlib.Path.open")
+ mocker.patch("json.load", return_value=response)
+
+ watcher._cache_load()
+ assert not watcher.known
+
+
+def test_cache_save(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must save state to cache
+ """
+ mocker.patch("pathlib.Path.open")
+ json_mock = mocker.patch("json.dump")
+
+ watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
+ watcher._cache_save()
+ json_mock.assert_called_once()
+
+
+def test_cache_save_failed(watcher: Watcher, mocker: MockerFixture) -> None:
+ """
+ must not fail on dumping packages
+ """
+ mocker.patch("pathlib.Path.open")
+ mocker.patch("json.dump", side_effect=Exception())
+
+ watcher._cache_save()
+
+
+def test_cache_save_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must save state to cache which can be loaded later
+ """
+ dump_file = Path(tempfile.mktemp())
+ mocker.patch("ahriman.core.status.watcher.Watcher.cache_path",
+ new_callable=PropertyMock, return_value=dump_file)
+ known_current = {package_ahriman.base: (package_ahriman, BuildStatus())}
+
+ watcher.known = known_current
+ watcher._cache_save()
+
+ watcher.known = {package_ahriman.base: (None, None)}
+ watcher._cache_load()
+ assert watcher.known == known_current
+
+ dump_file.unlink()
+
+
+def test_get(watcher: Watcher, package_ahriman: Package) -> None:
+ """
+ must return package status
+ """
+ watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
+ package, status = watcher.get(package_ahriman.base)
+ assert package == package_ahriman
+ assert status.status == BuildStatusEnum.Unknown
+
+
+def test_get_failed(watcher: Watcher, package_ahriman: Package) -> None:
+ """
+ must fail on unknown package
+ """
+ with pytest.raises(UnknownPackage):
+ watcher.get(package_ahriman.base)
+
+
+def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must correctly load packages
+ """
+ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
+ cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_load")
+
+ watcher.load()
+ cache_mock.assert_called_once()
+ package, status = watcher.known[package_ahriman.base]
+ assert package == package_ahriman
+ assert status.status == BuildStatusEnum.Unknown
+
+
+def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must correctly load packages with known statuses
+ """
+ mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
+ mocker.patch("ahriman.core.status.watcher.Watcher._cache_load")
+ watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus(BuildStatusEnum.Success))}
+
+ watcher.load()
+ _, status = watcher.known[package_ahriman.base]
+ assert status.status == BuildStatusEnum.Success
+
+
+def test_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must remove package base
+ """
+ cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
+ watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
+
+ watcher.remove(package_ahriman.base)
+ assert not watcher.known
+ cache_mock.assert_called_once()
+
+
+def test_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must not fail on unknown base removal
+ """
+ cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
+
+ watcher.remove(package_ahriman.base)
+ cache_mock.assert_called_once()
+
+
+def test_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must update package status
+ """
+ cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
+
+ watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, package_ahriman)
+ cache_mock.assert_called_once()
+ package, status = watcher.known[package_ahriman.base]
+ assert package == package_ahriman
+ assert status.status == BuildStatusEnum.Unknown
+
+
+def test_update_ping(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must update package status only for known package
+ """
+ cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
+ watcher.known = {package_ahriman.base: (package_ahriman, BuildStatus())}
+
+ watcher.update(package_ahriman.base, BuildStatusEnum.Success, None)
+ cache_mock.assert_called_once()
+ package, status = watcher.known[package_ahriman.base]
+ assert package == package_ahriman
+ assert status.status == BuildStatusEnum.Success
+
+
+def test_update_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must fail on unknown package status update only
+ """
+ cache_mock = mocker.patch("ahriman.core.status.watcher.Watcher._cache_save")
+
+ with pytest.raises(UnknownPackage):
+ watcher.update(package_ahriman.base, BuildStatusEnum.Unknown, None)
+ cache_mock.assert_called_once()
+
+
+def test_update_self(watcher: Watcher) -> None:
+ """
+ must update service status
+ """
+ watcher.update_self(BuildStatusEnum.Success)
+ assert watcher.status.status == BuildStatusEnum.Success
diff --git a/tests/ahriman/core/status/test_web_client.py b/tests/ahriman/core/status/test_web_client.py
new file mode 100644
index 00000000..8d5cc2e7
--- /dev/null
+++ b/tests/ahriman/core/status/test_web_client.py
@@ -0,0 +1,163 @@
+import json
+import pytest
+
+from pytest_mock import MockerFixture
+from requests import Response
+
+from ahriman.core.status.web_client import WebClient
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+from ahriman.models.package import Package
+
+
+def test_ahriman_url(web_client: WebClient) -> None:
+ """
+ must generate service status url correctly
+ """
+ assert web_client._ahriman_url().startswith(f"http://{web_client.host}:{web_client.port}")
+ assert web_client._ahriman_url().endswith("/api/v1/ahriman")
+
+
+def test_package_url(web_client: WebClient, package_ahriman: Package) -> None:
+ """
+ must generate package status correctly
+ """
+ assert web_client._package_url(package_ahriman.base).startswith(f"http://{web_client.host}:{web_client.port}")
+ assert web_client._package_url(package_ahriman.base).endswith(f"/api/v1/packages/{package_ahriman.base}")
+
+
+def test_add(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must process package addition
+ """
+ requests_mock = mocker.patch("requests.post")
+ payload = pytest.helpers.get_package_status(package_ahriman)
+
+ web_client.add(package_ahriman, BuildStatusEnum.Unknown)
+ requests_mock.assert_called_with(pytest.helpers.anyvar(str, True), json=payload)
+
+
+def test_add_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during addition
+ """
+ mocker.patch("requests.post", side_effect=Exception())
+ web_client.add(package_ahriman, BuildStatusEnum.Unknown)
+
+
+def test_get_all(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must return all packages status
+ """
+ response = [pytest.helpers.get_package_status_extended(package_ahriman)]
+ response_obj = Response()
+ response_obj._content = json.dumps(response).encode("utf8")
+ response_obj.status_code = 200
+
+ requests_mock = mocker.patch("requests.get", return_value=response_obj)
+
+ result = web_client.get(None)
+ requests_mock.assert_called_once()
+ assert len(result) == len(response)
+ assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
+
+
+def test_get_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during status getting
+ """
+ mocker.patch("requests.get", side_effect=Exception())
+ assert web_client.get(None) == []
+
+
+def test_get_single(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must return single package status
+ """
+ response = [pytest.helpers.get_package_status_extended(package_ahriman)]
+ response_obj = Response()
+ response_obj._content = json.dumps(response).encode("utf8")
+ response_obj.status_code = 200
+
+ requests_mock = mocker.patch("requests.get", return_value=response_obj)
+
+ result = web_client.get(package_ahriman.base)
+ requests_mock.assert_called_once()
+ assert len(result) == len(response)
+ assert (package_ahriman, BuildStatusEnum.Unknown) in [(package, status.status) for package, status in result]
+
+
+def test_get_self(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must return service status
+ """
+ response_obj = Response()
+ response_obj._content = json.dumps(BuildStatus().view()).encode("utf8")
+ response_obj.status_code = 200
+
+ requests_mock = mocker.patch("requests.get", return_value=response_obj)
+
+ result = web_client.get_self()
+ requests_mock.assert_called_once()
+ assert result.status == BuildStatusEnum.Unknown
+
+
+def test_get_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during service status getting
+ """
+ mocker.patch("requests.get", side_effect=Exception())
+ assert web_client.get_self().status == BuildStatusEnum.Unknown
+
+
+def test_remove(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must process package removal
+ """
+ requests_mock = mocker.patch("requests.delete")
+
+ web_client.remove(package_ahriman.base)
+ requests_mock.assert_called_with(pytest.helpers.anyvar(str, True))
+
+
+def test_remove_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during removal
+ """
+ mocker.patch("requests.delete", side_effect=Exception())
+ web_client.remove(package_ahriman.base)
+
+
+def test_update(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must process package update
+ """
+ requests_mock = mocker.patch("requests.post")
+
+ web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
+ requests_mock.assert_called_with(pytest.helpers.anyvar(str, True), json={"status": BuildStatusEnum.Unknown.value})
+
+
+def test_update_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during update
+ """
+ mocker.patch("requests.post", side_effect=Exception())
+ web_client.update(package_ahriman.base, BuildStatusEnum.Unknown)
+
+
+def test_update_self(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must process service update
+ """
+ requests_mock = mocker.patch("requests.post")
+
+ web_client.update_self(BuildStatusEnum.Unknown)
+ requests_mock.assert_called_with(pytest.helpers.anyvar(str, True), json={"status": BuildStatusEnum.Unknown.value})
+
+
+def test_update_self_failed(web_client: WebClient, mocker: MockerFixture) -> None:
+ """
+ must suppress any exception happened during service update
+ """
+ mocker.patch("requests.post", side_effect=Exception())
+ web_client.update_self(BuildStatusEnum.Unknown)
diff --git a/tests/ahriman/core/test_configuration.py b/tests/ahriman/core/test_configuration.py
new file mode 100644
index 00000000..e593b29c
--- /dev/null
+++ b/tests/ahriman/core/test_configuration.py
@@ -0,0 +1,129 @@
+from pathlib import Path
+
+from pytest_mock import MockerFixture
+
+from ahriman.core.configuration import Configuration
+
+
+def test_from_path(mocker: MockerFixture) -> None:
+ """
+ must load configuration
+ """
+ read_mock = mocker.patch("configparser.RawConfigParser.read")
+ load_includes_mock = mocker.patch("ahriman.core.configuration.Configuration.load_includes")
+ load_logging_mock = mocker.patch("ahriman.core.configuration.Configuration.load_logging")
+ path = Path("path")
+
+ config = Configuration.from_path(path, True)
+ assert config.path == path
+ read_mock.assert_called_with(path)
+ load_includes_mock.assert_called_once()
+ load_logging_mock.assert_called_once()
+
+
+def test_absolute_path_for_absolute(configuration: Configuration) -> None:
+ """
+ must not change path for absolute path in settings
+ """
+ path = Path("/a/b/c")
+ configuration.set("build", "path", str(path))
+ assert configuration.getpath("build", "path") == path
+
+
+def test_absolute_path_for_relative(configuration: Configuration) -> None:
+ """
+ must prepend root path to relative path
+ """
+ path = Path("a")
+ configuration.set("build", "path", str(path))
+ result = configuration.getpath("build", "path")
+ assert result.is_absolute()
+ assert result.parent == configuration.path.parent
+ assert result.name == path.name
+
+
+def test_dump(configuration: Configuration) -> None:
+ """
+ dump must not be empty
+ """
+ assert configuration.dump("x86_64")
+
+
+def test_dump_architecture_specific(configuration: Configuration) -> None:
+ """
+ dump must contain architecture specific settings
+ """
+ configuration.add_section("build_x86_64")
+ configuration.set("build_x86_64", "archbuild_flags", "")
+
+ dump = configuration.dump("x86_64")
+ assert dump
+ assert "build" not in dump
+ assert "build_x86_64" in dump
+
+
+def test_getlist(configuration: Configuration) -> None:
+ """
+ must return list of string correctly
+ """
+ configuration.set("build", "test_list", "a b c")
+ assert configuration.getlist("build", "test_list") == ["a", "b", "c"]
+
+
+def test_getlist_empty(configuration: Configuration) -> None:
+ """
+ must return list of string correctly for non-existing option
+ """
+ assert configuration.getlist("build", "test_list") == []
+ configuration.set("build", "test_list", "")
+ assert configuration.getlist("build", "test_list") == []
+
+
+def test_getlist_single(configuration: Configuration) -> None:
+ """
+ must return list of strings for single string
+ """
+ configuration.set("build", "test_list", "a")
+ assert configuration.getlist("build", "test_list") == ["a"]
+
+
+def test_get_section_name(configuration: Configuration) -> None:
+ """
+ must return architecture specific group
+ """
+ configuration.add_section("build_x86_64")
+ configuration.set("build_x86_64", "archbuild_flags", "")
+ assert configuration.get_section_name("build", "x86_64") == "build_x86_64"
+
+
+def test_get_section_name_missing(configuration: Configuration) -> None:
+ """
+ must return default group if architecture depending group does not exist
+ """
+ assert configuration.get_section_name("prefix", "suffix") == "prefix"
+ assert configuration.get_section_name("build", "x86_64") == "build"
+
+
+def test_load_includes_missing(configuration: Configuration) -> None:
+ """
+ must not fail if not include directory found
+ """
+ configuration.set("settings", "include", "path")
+ configuration.load_includes()
+
+
+def test_load_logging_fallback(configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must fallback to stderr without errors
+ """
+ mocker.patch("logging.config.fileConfig", side_effect=PermissionError())
+ configuration.load_logging(True)
+
+
+def test_load_logging_stderr(configuration: Configuration, mocker: MockerFixture) -> None:
+ """
+ must use stderr if flag set
+ """
+ logging_mock = mocker.patch("logging.config.fileConfig")
+ configuration.load_logging(False)
+ logging_mock.assert_not_called()
diff --git a/tests/ahriman/core/test_tree.py b/tests/ahriman/core/test_tree.py
new file mode 100644
index 00000000..f6dc7069
--- /dev/null
+++ b/tests/ahriman/core/test_tree.py
@@ -0,0 +1,78 @@
+from pytest_mock import MockerFixture
+
+from ahriman.core.tree import Leaf, Tree
+from ahriman.models.package import Package
+
+
+def test_leaf_is_root_empty(leaf_ahriman: Leaf) -> None:
+ """
+ must be root for empty packages list
+ """
+ assert leaf_ahriman.is_root([])
+
+
+def test_leaf_is_root_false(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
+ """
+ must be root for empty dependencies list or if does not depend on packages
+ """
+ assert leaf_ahriman.is_root([leaf_python_schedule])
+ leaf_ahriman.dependencies = {"ahriman-dependency"}
+ assert leaf_ahriman.is_root([leaf_python_schedule])
+
+
+def test_leaf_is_root_true(leaf_ahriman: Leaf, leaf_python_schedule: Leaf) -> None:
+ """
+ must not be root if depends on packages
+ """
+ leaf_ahriman.dependencies = {"python-schedule"}
+ assert not leaf_ahriman.is_root([leaf_python_schedule])
+
+ leaf_ahriman.dependencies = {"python2-schedule"}
+ assert not leaf_ahriman.is_root([leaf_python_schedule])
+
+ leaf_ahriman.dependencies = set(leaf_python_schedule.package.packages.keys())
+ assert not leaf_ahriman.is_root([leaf_python_schedule])
+
+
+def test_leaf_load(package_ahriman: Package, mocker: MockerFixture) -> None:
+ """
+ must load with dependencies
+ """
+ tempdir_mock = mocker.patch("tempfile.mkdtemp")
+ fetch_mock = mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+ dependencies_mock = mocker.patch("ahriman.models.package.Package.dependencies", return_value={"ahriman-dependency"})
+ rmtree_mock = mocker.patch("shutil.rmtree")
+
+ leaf = Leaf.load(package_ahriman)
+ assert leaf.package == package_ahriman
+ assert leaf.dependencies == {"ahriman-dependency"}
+ tempdir_mock.assert_called_once()
+ fetch_mock.assert_called_once()
+ dependencies_mock.assert_called_once()
+ rmtree_mock.assert_called_once()
+
+
+def test_tree_levels(leaf_ahriman: Leaf, leaf_python_schedule: Leaf, mocker: MockerFixture) -> None:
+ """
+ must generate correct levels in the simples case
+ """
+ leaf_ahriman.dependencies = set(leaf_python_schedule.package.packages.keys())
+
+ tree = Tree([leaf_ahriman, leaf_python_schedule])
+ assert len(tree.levels()) == 2
+ first, second = tree.levels()
+ assert first == [leaf_python_schedule.package]
+ assert second == [leaf_ahriman.package]
+
+
+def test_tree_load(package_ahriman: Package, package_python_schedule: Package, mocker: MockerFixture) -> None:
+ """
+ must package list
+ """
+ mocker.patch("tempfile.mkdtemp")
+ mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+ mocker.patch("ahriman.models.package.Package.dependencies")
+ mocker.patch("shutil.rmtree")
+
+ tree = Tree.load([package_ahriman, package_python_schedule])
+ assert len(tree.leaves) == 2
diff --git a/tests/ahriman/core/test_util.py b/tests/ahriman/core/test_util.py
new file mode 100644
index 00000000..e79ce29e
--- /dev/null
+++ b/tests/ahriman/core/test_util.py
@@ -0,0 +1,131 @@
+import logging
+import pytest
+import subprocess
+
+from pytest_mock import MockerFixture
+
+from ahriman.core.util import check_output, package_like, pretty_datetime, pretty_size
+from ahriman.models.package import Package
+
+
+def test_check_output(mocker: MockerFixture) -> None:
+ """
+ must run command and log result
+ """
+ logger_mock = mocker.patch("logging.Logger.debug")
+
+ assert check_output("echo", "hello", exception=None) == "hello"
+ logger_mock.assert_not_called()
+
+ assert check_output("echo", "hello", exception=None, logger=logging.getLogger("")) == "hello"
+ logger_mock.assert_called_once()
+
+
+def test_check_output_failure(mocker: MockerFixture) -> None:
+ """
+ must process exception correctly
+ """
+ logger_mock = mocker.patch("logging.Logger.debug")
+ mocker.patch("subprocess.check_output", side_effect=subprocess.CalledProcessError(1, "echo"))
+
+ with pytest.raises(subprocess.CalledProcessError):
+ check_output("echo", "hello", exception=None)
+ logger_mock.assert_not_called()
+
+ with pytest.raises(subprocess.CalledProcessError):
+ check_output("echo", "hello", exception=None, logger=logging.getLogger(""))
+ logger_mock.assert_not_called()
+
+
+def test_check_output_failure_log(mocker: MockerFixture) -> None:
+ """
+ must process exception correctly and log it
+ """
+ logger_mock = mocker.patch("logging.Logger.debug")
+ mocker.patch("subprocess.check_output", side_effect=subprocess.CalledProcessError(1, "echo", output=b"result"))
+
+ with pytest.raises(subprocess.CalledProcessError):
+ check_output("echo", "hello", exception=None, logger=logging.getLogger(""))
+ logger_mock.assert_called_once()
+
+
+def test_package_like(package_ahriman: Package) -> None:
+ """
+ package_like must return true for archives
+ """
+ assert package_like(package_ahriman.packages[package_ahriman.base].filepath)
+
+
+def test_package_like_sig(package_ahriman: Package) -> None:
+ """
+ package_like must return false for signature files
+ """
+ package_file = package_ahriman.packages[package_ahriman.base].filepath
+ sig_file = package_file.parent / f"{package_file.name}.sig"
+ assert not package_like(sig_file)
+
+
+def test_pretty_datetime() -> None:
+ """
+ must generate string from timestamp value
+ """
+ assert pretty_datetime(0) == "1970-01-01 00:00:00"
+
+
+def test_pretty_datetime_empty() -> None:
+ """
+ must generate empty string from None timestamp
+ """
+ assert pretty_datetime(None) == ""
+
+
+def test_pretty_size_bytes() -> None:
+ """
+ must generate bytes string for bytes value
+ """
+ value, abbrev = pretty_size(42).split()
+ assert value == "42.0"
+ assert abbrev == "B"
+
+
+def test_pretty_size_kbytes() -> None:
+ """
+ must generate kibibytes string for kibibytes value
+ """
+ value, abbrev = pretty_size(42 * 1024).split()
+ assert value == "42.0"
+ assert abbrev == "KiB"
+
+
+def test_pretty_size_mbytes() -> None:
+ """
+ must generate mebibytes string for mebibytes value
+ """
+ value, abbrev = pretty_size(42 * 1024 * 1024).split()
+ assert value == "42.0"
+ assert abbrev == "MiB"
+
+
+def test_pretty_size_gbytes() -> None:
+ """
+ must generate gibibytes string for gibibytes value
+ """
+ value, abbrev = pretty_size(42 * 1024 * 1024 * 1024).split()
+ assert value == "42.0"
+ assert abbrev == "GiB"
+
+
+def test_pretty_size_pbytes() -> None:
+ """
+ must generate pebibytes string for pebibytes value
+ """
+ value, abbrev = pretty_size(42 * 1024 * 1024 * 1024 * 1024).split()
+ assert value == "43008.0"
+ assert abbrev == "GiB"
+
+
+def test_pretty_size_empty() -> None:
+ """
+ must generate empty string for None value
+ """
+ assert pretty_size(None) == ""
diff --git a/tests/ahriman/core/upload/test_rsync.py b/tests/ahriman/core/upload/test_rsync.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/core/upload/test_s3.py b/tests/ahriman/core/upload/test_s3.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/core/upload/test_uploader.py b/tests/ahriman/core/upload/test_uploader.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/models/conftest.py b/tests/ahriman/models/conftest.py
new file mode 100644
index 00000000..cef09533
--- /dev/null
+++ b/tests/ahriman/models/conftest.py
@@ -0,0 +1,19 @@
+import pytest
+
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+from ahriman.models.package import Package
+from ahriman.models.package_desciption import PackageDescription
+
+
+@pytest.fixture
+def build_status_failed() -> BuildStatus:
+ return BuildStatus(BuildStatusEnum.Failed, 42)
+
+
+@pytest.fixture
+def package_tpacpi_bat_git() -> Package:
+ return Package(
+ base="tpacpi-bat-git",
+ version="3.1.r12.g4959b52-1",
+ aur_url="https://aur.archlinux.org",
+ packages={"tpacpi-bat-git": PackageDescription()})
diff --git a/tests/ahriman/models/test_build_status.py b/tests/ahriman/models/test_build_status.py
new file mode 100644
index 00000000..f7c78cf2
--- /dev/null
+++ b/tests/ahriman/models/test_build_status.py
@@ -0,0 +1,38 @@
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+
+
+def test_build_status_enum_badges_color() -> None:
+ """
+ status color must be one of shields.io supported
+ """
+ SUPPORTED_COLORS = [
+ "brightgreen", "green", "yellowgreen", "yellow", "orange", "red", "blue", "lightgrey",
+ "success", "important", "critical", "informational", "inactive", "blueviolet"
+ ]
+
+ for status in BuildStatusEnum:
+ assert status.badges_color() in SUPPORTED_COLORS
+
+
+def test_build_status_init_1() -> None:
+ """
+ must construct status object from None
+ """
+ status = BuildStatus()
+ assert status.status == BuildStatusEnum.Unknown
+ assert status.timestamp > 0
+
+
+def test_build_status_init_2(build_status_failed: BuildStatus) -> None:
+ """
+ must construct status object from objects
+ """
+ status = BuildStatus(BuildStatusEnum.Failed, 42)
+ assert status == build_status_failed
+
+
+def test_build_status_from_json_view(build_status_failed: BuildStatus) -> None:
+ """
+ must construct same object from json
+ """
+ assert BuildStatus.from_json(build_status_failed.view()) == build_status_failed
diff --git a/tests/ahriman/models/test_package.py b/tests/ahriman/models/test_package.py
new file mode 100644
index 00000000..cb3b9640
--- /dev/null
+++ b/tests/ahriman/models/test_package.py
@@ -0,0 +1,130 @@
+from pathlib import Path
+from pytest_mock import MockerFixture
+
+from ahriman.models.package import Package
+from ahriman.models.repository_paths import RepositoryPaths
+
+
+def test_git_url(package_ahriman: Package) -> None:
+ """
+ must generate valid git url
+ """
+ assert package_ahriman.git_url.endswith(".git")
+ assert package_ahriman.git_url.startswith(package_ahriman.aur_url)
+ assert package_ahriman.base in package_ahriman.git_url
+
+
+def test_is_single_package_false(package_python_schedule: Package) -> None:
+ """
+ python-schedule must not be single package
+ """
+ assert not package_python_schedule.is_single_package
+
+
+def test_is_single_package_true(package_ahriman: Package) -> None:
+ """
+ ahriman must be single package
+ """
+ assert package_ahriman.is_single_package
+
+
+def test_is_vcs_false(package_ahriman: Package) -> None:
+ """
+ ahriman must not be VCS package
+ """
+ assert not package_ahriman.is_vcs
+
+
+def test_is_vcs_true(package_tpacpi_bat_git: Package) -> None:
+ """
+ tpacpi-bat-git must be VCS package
+ """
+ assert package_tpacpi_bat_git.is_vcs
+
+
+def test_web_url(package_ahriman: Package) -> None:
+ """
+ must generate valid web url
+ """
+ assert package_ahriman.web_url.startswith(package_ahriman.aur_url)
+ assert package_ahriman.base in package_ahriman.web_url
+
+
+def test_from_json_view_1(package_ahriman: Package) -> None:
+ """
+ must construct same object from json
+ """
+ assert Package.from_json(package_ahriman.view()) == package_ahriman
+
+
+def test_from_json_view_2(package_python_schedule: Package) -> None:
+ """
+ must construct same object from json
+ """
+ assert Package.from_json(package_python_schedule.view()) == package_python_schedule
+
+
+def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
+ """
+ must construct same object from json
+ """
+ assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git
+
+
+def test_dependencies_with_version(mocker: MockerFixture, resource_path_root: Path) -> None:
+ """
+ must load correct list of dependencies with version
+ """
+ srcinfo = (resource_path_root / "models" / "package_yay_srcinfo").read_text()
+
+ mocker.patch("pathlib.Path.read_text", return_value=srcinfo)
+
+ assert Package.dependencies(Path("path")) == {"git", "go", "pacman"}
+
+
+def test_actual_version(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
+ """
+ must return same actual_version as version is
+ """
+ assert package_ahriman.actual_version(repository_paths) == package_ahriman.version
+
+
+def test_actual_version_vcs(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
+ mocker: MockerFixture, resource_path_root: Path) -> None:
+ """
+ must return valid actual_version for VCS package
+ """
+ srcinfo = (resource_path_root / "models" / "package_tpacpi-bat-git_srcinfo").read_text()
+
+ mocker.patch("ahriman.models.package.Package._check_output", return_value=srcinfo)
+ mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+
+ assert package_tpacpi_bat_git.actual_version(repository_paths) == "3.1.r13.g4959b52-1"
+
+
+def test_actual_version_vcs_failed(package_tpacpi_bat_git: Package, repository_paths: RepositoryPaths,
+ mocker: MockerFixture) -> None:
+ """
+ must return same version in case if exception occurred
+ """
+ mocker.patch("ahriman.models.package.Package._check_output", side_effect=Exception())
+ mocker.patch("ahriman.core.build_tools.task.Task.fetch")
+
+ assert package_tpacpi_bat_git.actual_version(repository_paths) == package_tpacpi_bat_git.version
+
+
+def test_is_outdated_false(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
+ """
+ must be not outdated for the same package
+ """
+ assert not package_ahriman.is_outdated(package_ahriman, repository_paths)
+
+
+def test_is_outdated_true(package_ahriman: Package, repository_paths: RepositoryPaths) -> None:
+ """
+ must be outdated for the new version
+ """
+ other = Package.from_json(package_ahriman.view())
+ other.version = other.version.replace("-1", "-2")
+
+ assert package_ahriman.is_outdated(other, repository_paths)
diff --git a/tests/ahriman/models/test_package_desciption.py b/tests/ahriman/models/test_package_desciption.py
new file mode 100644
index 00000000..5808f752
--- /dev/null
+++ b/tests/ahriman/models/test_package_desciption.py
@@ -0,0 +1,17 @@
+from ahriman.models.package_desciption import PackageDescription
+
+
+def test_filepath(package_description_ahriman: PackageDescription) -> None:
+ """
+ must generate correct filepath if set
+ """
+ assert package_description_ahriman.filepath is not None
+ assert package_description_ahriman.filepath.name == package_description_ahriman.filename
+
+
+def test_filepath_empty(package_description_ahriman: PackageDescription) -> None:
+ """
+ must return None for missing filename
+ """
+ package_description_ahriman.filename = None
+ assert package_description_ahriman.filepath is None
diff --git a/tests/ahriman/models/test_report_settings.py b/tests/ahriman/models/test_report_settings.py
new file mode 100644
index 00000000..8fa53f5d
--- /dev/null
+++ b/tests/ahriman/models/test_report_settings.py
@@ -0,0 +1,20 @@
+import pytest
+
+from ahriman.core.exceptions import InvalidOption
+from ahriman.models.report_settings import ReportSettings
+
+
+def test_from_option_invalid() -> None:
+ """
+ must raise exception on invalid option
+ """
+ with pytest.raises(InvalidOption, match=".* `invalid`$"):
+ ReportSettings.from_option("invalid")
+
+
+def test_from_option_valid() -> None:
+ """
+ must return value from valid options
+ """
+ assert ReportSettings.from_option("html") == ReportSettings.HTML
+ assert ReportSettings.from_option("HTML") == ReportSettings.HTML
diff --git a/tests/ahriman/models/test_repository_paths.py b/tests/ahriman/models/test_repository_paths.py
new file mode 100644
index 00000000..4f9226fd
--- /dev/null
+++ b/tests/ahriman/models/test_repository_paths.py
@@ -0,0 +1,23 @@
+from pytest_mock import MockerFixture
+from unittest import mock
+
+from ahriman.models.repository_paths import RepositoryPaths
+
+
+def test_create_tree(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
+ """
+ must create whole tree
+ """
+ paths = {
+ prop
+ for prop in dir(repository_paths)
+ if not prop.startswith("_") and prop not in ("architecture", "create_tree", "root")
+ }
+ mkdir_mock = mocker.patch("pathlib.Path.mkdir")
+
+ repository_paths.create_tree()
+ mkdir_mock.assert_has_calls(
+ [
+ mock.call(mode=0o755, parents=True, exist_ok=True)
+ for _ in paths
+ ])
diff --git a/tests/ahriman/models/test_sign_settings.py b/tests/ahriman/models/test_sign_settings.py
new file mode 100644
index 00000000..5555e5fd
--- /dev/null
+++ b/tests/ahriman/models/test_sign_settings.py
@@ -0,0 +1,26 @@
+import pytest
+
+from ahriman.core.exceptions import InvalidOption
+from ahriman.models.sign_settings import SignSettings
+
+
+def test_from_option_invalid() -> None:
+ """
+ must raise exception on invalid option
+ """
+ with pytest.raises(InvalidOption, match=".* `invalid`$"):
+ SignSettings.from_option("invalid")
+
+
+def test_from_option_valid() -> None:
+ """
+ must return value from valid options
+ """
+ assert SignSettings.from_option("package") == SignSettings.SignPackages
+ assert SignSettings.from_option("PACKAGE") == SignSettings.SignPackages
+ assert SignSettings.from_option("packages") == SignSettings.SignPackages
+ assert SignSettings.from_option("sign-package") == SignSettings.SignPackages
+
+ assert SignSettings.from_option("repository") == SignSettings.SignRepository
+ assert SignSettings.from_option("REPOSITORY") == SignSettings.SignRepository
+ assert SignSettings.from_option("sign-repository") == SignSettings.SignRepository
diff --git a/tests/ahriman/models/test_upload_settings.py b/tests/ahriman/models/test_upload_settings.py
new file mode 100644
index 00000000..cdf25f7a
--- /dev/null
+++ b/tests/ahriman/models/test_upload_settings.py
@@ -0,0 +1,23 @@
+import pytest
+
+from ahriman.core.exceptions import InvalidOption
+from ahriman.models.upload_settings import UploadSettings
+
+
+def test_from_option_invalid() -> None:
+ """
+ must raise exception on invalid option
+ """
+ with pytest.raises(InvalidOption, match=".* `invalid`$"):
+ UploadSettings.from_option("invalid")
+
+
+def test_from_option_valid() -> None:
+ """
+ must return value from valid options
+ """
+ assert UploadSettings.from_option("rsync") == UploadSettings.Rsync
+ assert UploadSettings.from_option("RSYNC") == UploadSettings.Rsync
+
+ assert UploadSettings.from_option("s3") == UploadSettings.S3
+ assert UploadSettings.from_option("S3") == UploadSettings.S3
diff --git a/tests/ahriman/web/conftest.py b/tests/ahriman/web/conftest.py
new file mode 100644
index 00000000..2dc774d7
--- /dev/null
+++ b/tests/ahriman/web/conftest.py
@@ -0,0 +1,13 @@
+import pytest
+
+from aiohttp import web
+from pytest_mock import MockerFixture
+
+from ahriman.core.configuration import Configuration
+from ahriman.web.web import setup_service
+
+
+@pytest.fixture
+def application(configuration: Configuration, mocker: MockerFixture) -> web.Application:
+ mocker.patch("pathlib.Path.mkdir")
+ return setup_service("x86_64", configuration)
diff --git a/tests/ahriman/web/middlewares/test_exception_handler.py b/tests/ahriman/web/middlewares/test_exception_handler.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/web/test_routes.py b/tests/ahriman/web/test_routes.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ahriman/web/test_web.py b/tests/ahriman/web/test_web.py
new file mode 100644
index 00000000..e5c77100
--- /dev/null
+++ b/tests/ahriman/web/test_web.py
@@ -0,0 +1,45 @@
+import pytest
+
+from aiohttp import web
+from pytest_mock import MockerFixture
+
+from ahriman.core.exceptions import InitializeException
+from ahriman.core.status.watcher import Watcher
+from ahriman.web.web import on_startup, run_server
+
+
+async def test_on_startup(application: web.Application, watcher: Watcher, mocker: MockerFixture) -> None:
+ """
+ must call load method
+ """
+ mocker.patch("aiohttp.web.Application.__getitem__", return_value=watcher)
+ load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
+
+ await on_startup(application)
+ load_mock.assert_called_once()
+
+
+async def test_on_startup_exception(application: web.Application, watcher: Watcher, mocker: MockerFixture) -> None:
+ """
+ must throw exception on load error
+ """
+ mocker.patch("aiohttp.web.Application.__getitem__", return_value=watcher)
+ mocker.patch("ahriman.core.status.watcher.Watcher.load", side_effect=Exception())
+
+ with pytest.raises(InitializeException):
+ await on_startup(application)
+
+
+def test_run(application: web.Application, mocker: MockerFixture) -> None:
+ """
+ must run application
+ """
+ host = "localhost"
+ port = 8080
+ application["config"].set("web", "host", host)
+ application["config"].set("web", "port", str(port))
+ run_app_mock = mocker.patch("aiohttp.web.run_app")
+
+ run_server(application)
+ run_app_mock.assert_called_with(application, host=host, port=port,
+ handle_signals=False, access_log=pytest.helpers.anyvar(int))
diff --git a/tests/ahriman/web/views/conftest.py b/tests/ahriman/web/views/conftest.py
new file mode 100644
index 00000000..31f3da77
--- /dev/null
+++ b/tests/ahriman/web/views/conftest.py
@@ -0,0 +1,14 @@
+import pytest
+
+from aiohttp import web
+from asyncio import BaseEventLoop
+from pytest_aiohttp import TestClient
+from pytest_mock import MockerFixture
+from typing import Any
+
+
+@pytest.fixture
+def client(application: web.Application, loop: BaseEventLoop,
+ aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
+ mocker.patch("pathlib.Path.iterdir", return_value=[])
+ return loop.run_until_complete(aiohttp_client(application))
diff --git a/tests/ahriman/web/views/test_view_ahriman.py b/tests/ahriman/web/views/test_view_ahriman.py
new file mode 100644
index 00000000..250eb731
--- /dev/null
+++ b/tests/ahriman/web/views/test_view_ahriman.py
@@ -0,0 +1,37 @@
+from aiohttp.test_utils import TestClient
+
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+
+
+async def test_get(client: TestClient) -> None:
+ """
+ must return valid service status
+ """
+ response = await client.get("/api/v1/ahriman")
+ status = BuildStatus.from_json(await response.json())
+
+ assert response.status == 200
+ assert status.status == BuildStatusEnum.Unknown
+
+
+async def test_post(client: TestClient) -> None:
+ """
+ must update service status correctly
+ """
+ payload = {"status": BuildStatusEnum.Success.value}
+ post_response = await client.post("/api/v1/ahriman", json=payload)
+ assert post_response.status == 204
+
+ response = await client.get("/api/v1/ahriman")
+ status = BuildStatus.from_json(await response.json())
+
+ assert response.status == 200
+ assert status.status == BuildStatusEnum.Success
+
+
+async def test_post_exception(client: TestClient) -> None:
+ """
+ must raise exception on invalid payload
+ """
+ post_response = await client.post("/api/v1/ahriman", json={})
+ assert post_response.status == 400
diff --git a/tests/ahriman/web/views/test_view_index.py b/tests/ahriman/web/views/test_view_index.py
new file mode 100644
index 00000000..1d2099fa
--- /dev/null
+++ b/tests/ahriman/web/views/test_view_index.py
@@ -0,0 +1,19 @@
+from pytest_aiohttp import TestClient
+
+
+async def test_get(client: TestClient) -> None:
+ """
+ must generate status page correctly (/)
+ """
+ response = await client.get("/")
+ assert response.status == 200
+ assert await response.text()
+
+
+async def test_get_index(client: TestClient) -> None:
+ """
+ must generate status page correctly (/index.html)
+ """
+ response = await client.get("/index.html")
+ assert response.status == 200
+ assert await response.text()
diff --git a/tests/ahriman/web/views/test_view_package.py b/tests/ahriman/web/views/test_view_package.py
new file mode 100644
index 00000000..d804a8a0
--- /dev/null
+++ b/tests/ahriman/web/views/test_view_package.py
@@ -0,0 +1,117 @@
+from pytest_aiohttp import TestClient
+
+from ahriman.models.build_status import BuildStatus, BuildStatusEnum
+from ahriman.models.package import Package
+
+
+async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
+ """
+ must return status for specific package
+ """
+ await client.post(f"/api/v1/packages/{package_ahriman.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
+ await client.post(f"/api/v1/packages/{package_python_schedule.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
+
+ response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 200
+
+ packages = [Package.from_json(item["package"]) for item in await response.json()]
+ assert packages
+ assert {package.base for package in packages} == {package_ahriman.base}
+
+
+async def test_get_not_found(client: TestClient, package_ahriman: Package) -> None:
+ """
+ must return Not Found for unknown package
+ """
+ response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 404
+
+
+async def test_delete(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
+ """
+ must delete single base
+ """
+ await client.post(f"/api/v1/packages/{package_ahriman.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
+ await client.post(f"/api/v1/packages/{package_python_schedule.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
+
+ response = await client.delete(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 204
+
+ response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 404
+
+ response = await client.get(f"/api/v1/packages/{package_python_schedule.base}")
+ assert response.status == 200
+
+
+async def test_delete_unknown(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
+ """
+ must suppress errors on unknown package deletion
+ """
+ await client.post(f"/api/v1/packages/{package_python_schedule.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
+
+ response = await client.delete(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 204
+
+ response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 404
+
+ response = await client.get(f"/api/v1/packages/{package_python_schedule.base}")
+ assert response.status == 200
+
+
+async def test_post(client: TestClient, package_ahriman: Package) -> None:
+ """
+ must update package status
+ """
+ post_response = await client.post(
+ f"/api/v1/packages/{package_ahriman.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
+ assert post_response.status == 204
+
+ response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 200
+
+
+async def test_post_exception(client: TestClient, package_ahriman: Package) -> None:
+ """
+ must raise exception on invalid payload
+ """
+ post_response = await client.post(f"/api/v1/packages/{package_ahriman.base}", json={})
+ assert post_response.status == 400
+
+
+async def test_post_light(client: TestClient, package_ahriman: Package) -> None:
+ """
+ must update package status only
+ """
+ post_response = await client.post(
+ f"/api/v1/packages/{package_ahriman.base}",
+ json={"status": BuildStatusEnum.Unknown.value, "package": package_ahriman.view()})
+ assert post_response.status == 204
+
+ post_response = await client.post(
+ f"/api/v1/packages/{package_ahriman.base}", json={"status": BuildStatusEnum.Success.value})
+ assert post_response.status == 204
+
+ response = await client.get(f"/api/v1/packages/{package_ahriman.base}")
+ assert response.status == 200
+ statuses = {
+ Package.from_json(item["package"]).base: BuildStatus.from_json(item["status"])
+ for item in await response.json()
+ }
+ assert statuses[package_ahriman.base].status == BuildStatusEnum.Success
+
+
+async def test_post_not_found(client: TestClient, package_ahriman: Package) -> None:
+ """
+ must raise exception on status update for unknown package
+ """
+ post_response = await client.post(
+ f"/api/v1/packages/{package_ahriman.base}", json={"status": BuildStatusEnum.Success.value})
+ assert post_response.status == 400
diff --git a/tests/ahriman/web/views/test_view_packages.py b/tests/ahriman/web/views/test_view_packages.py
new file mode 100644
index 00000000..a1691b65
--- /dev/null
+++ b/tests/ahriman/web/views/test_view_packages.py
@@ -0,0 +1,32 @@
+from pytest_aiohttp import TestClient
+from pytest_mock import MockerFixture
+
+from ahriman.models.build_status import BuildStatusEnum
+from ahriman.models.package import Package
+
+
+async def test_get(client: TestClient, package_ahriman: Package, package_python_schedule: Package) -> None:
+ """
+ must return status for all packages
+ """
+ await client.post(f"/api/v1/packages/{package_ahriman.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
+ await client.post(f"/api/v1/packages/{package_python_schedule.base}",
+ json={"status": BuildStatusEnum.Success.value, "package": package_python_schedule.view()})
+
+ response = await client.get("/api/v1/packages")
+ assert response.status == 200
+
+ packages = [Package.from_json(item["package"]) for item in await response.json()]
+ assert packages
+ assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
+
+
+async def test_post(client: TestClient, mocker: MockerFixture) -> None:
+ """
+ must be able to reload packages
+ """
+ load_mock = mocker.patch("ahriman.core.status.watcher.Watcher.load")
+ response = await client.post("/api/v1/packages")
+ assert response.status == 204
+ load_mock.assert_called_once()
diff --git a/tests/testresources/core/ahriman.ini b/tests/testresources/core/ahriman.ini
new file mode 100644
index 00000000..b918f520
--- /dev/null
+++ b/tests/testresources/core/ahriman.ini
@@ -0,0 +1,44 @@
+[settings]
+logging = logging.ini
+
+[alpm]
+aur_url = https://aur.archlinux.org
+database = /var/lib/pacman
+repositories = core extra community multilib
+root = /
+
+[build]
+archbuild_flags =
+build_command = extra-x86_64-build
+ignore_packages =
+makechrootpkg_flags =
+makepkg_flags = --skippgpcheck
+
+[repository]
+name = aur-clone
+root = /var/lib/ahriman
+
+[sign]
+target =
+key =
+
+[report]
+target =
+
+[html]
+path =
+homepage =
+link_path =
+template_path = ../web/templates/repo-index.jinja2
+
+[upload]
+target =
+
+[rsync]
+remote =
+
+[s3]
+bucket =
+
+[web]
+templates = ../web/templates
\ No newline at end of file
diff --git a/tests/testresources/core/logging.ini b/tests/testresources/core/logging.ini
new file mode 100644
index 00000000..fedc4897
--- /dev/null
+++ b/tests/testresources/core/logging.ini
@@ -0,0 +1,59 @@
+[loggers]
+keys = root,builder,build_details,http
+
+[handlers]
+keys = console_handler,build_file_handler,file_handler,http_handler
+
+[formatters]
+keys = generic_format
+
+[handler_console_handler]
+class = StreamHandler
+level = DEBUG
+formatter = generic_format
+args = (sys.stderr,)
+
+[handler_file_handler]
+class = logging.handlers.RotatingFileHandler
+level = DEBUG
+formatter = generic_format
+args = ("/var/log/ahriman/ahriman.log", "a", 20971520, 20)
+
+[handler_build_file_handler]
+class = logging.handlers.RotatingFileHandler
+level = DEBUG
+formatter = generic_format
+args = ("/var/log/ahriman/build.log", "a", 20971520, 20)
+
+[handler_http_handler]
+class = logging.handlers.RotatingFileHandler
+level = DEBUG
+formatter = generic_format
+args = ("/var/log/ahriman/http.log", "a", 20971520, 20)
+
+[formatter_generic_format]
+format = [%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d] [%(funcName)s]: %(message)s
+datefmt =
+
+[logger_root]
+level = DEBUG
+handlers = file_handler
+qualname = root
+
+[logger_builder]
+level = DEBUG
+handlers = file_handler
+qualname = builder
+propagate = 0
+
+[logger_build_details]
+level = DEBUG
+handlers = build_file_handler
+qualname = build_details
+propagate = 0
+
+[logger_http]
+level = DEBUG
+handlers = http_handler
+qualname = http
+propagate = 0
diff --git a/tests/testresources/models/package_tpacpi-bat-git_srcinfo b/tests/testresources/models/package_tpacpi-bat-git_srcinfo
new file mode 100644
index 00000000..58fddf92
--- /dev/null
+++ b/tests/testresources/models/package_tpacpi-bat-git_srcinfo
@@ -0,0 +1,17 @@
+pkgbase = tpacpi-bat-git
+ pkgdesc = A Perl script with ACPI calls for recent ThinkPads which are not supported by tp_smapi
+ pkgver = 3.1.r13.g4959b52
+ pkgrel = 1
+ url = https://github.com/teleshoes/tpacpi-bat
+ arch = any
+ license = GPL3
+ makedepends = git
+ depends = perl
+ depends = acpi_call
+ provides = tpacpi-bat
+ conflicts = tpacpi-bat
+ backup = etc/conf.d/tpacpi
+ source = git+https://github.com/teleshoes/tpacpi-bat.git
+ b2sums = SKIP
+
+pkgname = tpacpi-bat-git
diff --git a/tests/testresources/models/package_yay_srcinfo b/tests/testresources/models/package_yay_srcinfo
new file mode 100644
index 00000000..9d87b70b
--- /dev/null
+++ b/tests/testresources/models/package_yay_srcinfo
@@ -0,0 +1,21 @@
+pkgbase = yay
+ pkgdesc = Yet another yogurt. Pacman wrapper and AUR helper written in go.
+ pkgver = 10.2.0
+ pkgrel = 1
+ url = https://github.com/Jguer/yay
+ arch = i686
+ arch = pentium4
+ arch = x86_64
+ arch = arm
+ arch = armv7h
+ arch = armv6h
+ arch = aarch64
+ license = GPL3
+ makedepends = go
+ depends = pacman>5
+ depends = git
+ optdepends = sudo
+ source = yay-10.2.0.tar.gz::https://github.com/Jguer/yay/archive/v10.2.0.tar.gz
+ sha256sums = 755d049ec09cc20bdcbb004b12ab4e35ba3bb94a7dce9dfa544d24f87deda8aa
+
+pkgname = yay
diff --git a/tests/testresources/web/templates/build-status.jinja2 b/tests/testresources/web/templates/build-status.jinja2
new file mode 100644
index 00000000..3d36e9d1
--- /dev/null
+++ b/tests/testresources/web/templates/build-status.jinja2
@@ -0,0 +1,54 @@
+
+
+
+ {{ repository|e }}
+
+ {% include "style.jinja2" %}
+
+ {% include "sorttable.jinja2" %}
+ {% include "search.jinja2" %}
+
+
+
+
+
ahriman
+
+
+
+
+
+ {% include "search-line.jinja2" %}
+
+
+
+
+
+ {% for package in packages %}
+
+ {{ package.base|e }}
+ {{ package.packages|join(" "|safe) }}
+ {{ package.version|e }}
+ {{ package.timestamp|e }}
+ {{ package.status|e }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
diff --git a/tests/testresources/web/templates/repo-index.jinja2 b/tests/testresources/web/templates/repo-index.jinja2
new file mode 100644
index 00000000..3edbb86f
--- /dev/null
+++ b/tests/testresources/web/templates/repo-index.jinja2
@@ -0,0 +1,62 @@
+
+
+
+ {{ repository|e }}
+
+ {% include "style.jinja2" %}
+
+ {% include "sorttable.jinja2" %}
+ {% include "search.jinja2" %}
+
+
+
+
+
Archlinux user repository
+
+
+ {% if pgp_key is not none %}
+ This repository is signed with {{ pgp_key|e }} by default.
+ {% endif %}
+
+
+ $ cat /etc/pacman.conf
+ [{{ repository|e }}]
+ Server = {{ link_path|e }}
+ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Package{% if has_package_signed %}Required{% else %}Never{% endif %} TrustedOnly
+
+
+
+ {% include "search-line.jinja2" %}
+
+
+
+
+
+ {% for package in packages %}
+
+ {{ package.name|e }}
+ {{ package.version|e }}
+ {{ package.archive_size|e }}
+ {{ package.installed_size|e }}
+ {{ package.build_date|e }}
+
+ {% endfor %}
+
+
+
+
+
+ {% if homepage is not none %}
+ Homepage
+ {% endif %}
+
+
+
+
+
diff --git a/tests/testresources/web/templates/search-line.jinja2 b/tests/testresources/web/templates/search-line.jinja2
new file mode 100644
index 00000000..cb685672
--- /dev/null
+++ b/tests/testresources/web/templates/search-line.jinja2
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/tests/testresources/web/templates/search.jinja2 b/tests/testresources/web/templates/search.jinja2
new file mode 100644
index 00000000..a1c80092
--- /dev/null
+++ b/tests/testresources/web/templates/search.jinja2
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/tests/testresources/web/templates/sorttable.jinja2 b/tests/testresources/web/templates/sorttable.jinja2
new file mode 100644
index 00000000..12a1f08a
--- /dev/null
+++ b/tests/testresources/web/templates/sorttable.jinja2
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/tests/testresources/web/templates/style.jinja2 b/tests/testresources/web/templates/style.jinja2
new file mode 100644
index 00000000..26291537
--- /dev/null
+++ b/tests/testresources/web/templates/style.jinja2
@@ -0,0 +1,136 @@
+
\ No newline at end of file