diff --git a/src/ahriman/application/ahriman.py b/src/ahriman/application/ahriman.py index 3ac71b5c..f2552e17 100644 --- a/src/ahriman/application/ahriman.py +++ b/src/ahriman/application/ahriman.py @@ -23,9 +23,9 @@ import sys from multiprocessing import Pool +import ahriman.application.functions as functions import ahriman.version as version -from ahriman.application.application import Application from ahriman.application.lock import Lock from ahriman.core.configuration import Configuration @@ -47,118 +47,6 @@ def _call(args: argparse.Namespace, architecture: str, config: Configuration) -> return False -def add(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - add packages callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - Application(architecture, config).add(args.package, args.without_dependencies) - - -def clean(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - clean caches callback - :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) - - -def dump_config(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - configuration dump callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - del args - config_dump = config.dump(architecture) - for section, values in sorted(config_dump.items()): - print(f'[{section}]') - for key, value in sorted(values.items()): - print(f'{key} = {value}') - print() - - -def rebuild(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - world rebuild callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - del args - app = Application(architecture, config) - packages = app.repository.packages() - app.update(packages) - - -def remove(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - remove packages callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - Application(architecture, config).remove(args.package) - - -def report(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - generate report callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - Application(architecture, config).report(args.target) - - -def sync(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - sync to remote server callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - Application(architecture, config).sync(args.target) - - -def update(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - update packages callback - :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) - - application = Application(architecture, config) - packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn) - if args.dry_run: - return - - application.update(packages) - - -def web(args: argparse.Namespace, architecture: str, config: Configuration) -> None: - ''' - web server callback - :param args: command line args - :param architecture: repository architecture - :param config: configuration instance - ''' - del args - from ahriman.web.web import run_server, setup_service - application = setup_service(architecture, config) - run_server(application, architecture) - - if __name__ == '__main__': parser = argparse.ArgumentParser(prog='ahriman', description='ArcHlinux ReposItory MANager') parser.add_argument( @@ -176,14 +64,14 @@ if __name__ == '__main__': subparsers = parser.add_subparsers(title='command') add_parser = subparsers.add_parser('add', description='add package') - add_parser.add_argument('package', help='package name or archive path', nargs='+') + 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(fn=add) + add_parser.set_defaults(fn=functions.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 packages', nargs='*') + 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(fn=update, no_aur=False, no_manual=True, dry_run=True) + check_parser.set_defaults(fn=functions.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') @@ -194,37 +82,42 @@ if __name__ == '__main__': 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(fn=clean) + clean_parser.set_defaults(fn=functions.clean) config_parser = subparsers.add_parser('config', description='dump configuration for specified architecture') - config_parser.set_defaults(fn=dump_config) + config_parser.set_defaults(fn=functions.dump_config, lock=None, no_report=True) rebuild_parser = subparsers.add_parser('rebuild', description='rebuild whole repository') - rebuild_parser.set_defaults(fn=rebuild) + rebuild_parser.set_defaults(fn=functions.rebuild) remove_parser = subparsers.add_parser('remove', description='remove package') - remove_parser.add_argument('package', help='package name', nargs='+') - remove_parser.set_defaults(fn=remove) + remove_parser.add_argument('package', help='package name or base', nargs='+') + remove_parser.set_defaults(fn=functions.remove) report_parser = subparsers.add_parser('report', description='generate report') report_parser.add_argument('target', help='target to generate report', nargs='*') - report_parser.set_defaults(fn=report) + report_parser.set_defaults(fn=functions.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.set_defaults(fn=functions.status, lock=None, no_report=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.set_defaults(fn=sync) + sync_parser.set_defaults(fn=functions.sync) update_parser = subparsers.add_parser('update', description='run updates') - update_parser.add_argument('package', help='filter check by packages', nargs='*') + 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') - update_parser.set_defaults(fn=update) + update_parser.set_defaults(fn=functions.update) web_parser = subparsers.add_parser('web', description='start web server') - web_parser.set_defaults(fn=web, lock=None, no_report=True) + web_parser.set_defaults(fn=functions.web, lock=None, no_report=True) cmd_args = parser.parse_args() if 'fn' not in cmd_args: diff --git a/src/ahriman/application/application.py b/src/ahriman/application/application.py index 182b3409..b7874b1f 100644 --- a/src/ahriman/application/application.py +++ b/src/ahriman/application/application.py @@ -25,7 +25,7 @@ from typing import Callable, Iterable, List, Optional, Set from ahriman.core.build_tools.task import Task from ahriman.core.configuration import Configuration -from ahriman.repository.repository import Repository +from ahriman.core.repository.repository import Repository from ahriman.core.tree import Tree from ahriman.models.package import Package diff --git a/src/ahriman/application/functions.py b/src/ahriman/application/functions.py new file mode 100644 index 00000000..1cc7cfb3 --- /dev/null +++ b/src/ahriman/application/functions.py @@ -0,0 +1,163 @@ +# +# Copyright (c) 2021 Evgenii Alekseev. +# +# This file is part of ahriman +# (see https://github.com/arcan1s/ahriman). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +import argparse + +from typing import Iterable, Tuple + +from ahriman.application.application import Application +from ahriman.core.configuration import Configuration +from ahriman.models.build_status import BuildStatus +from ahriman.models.package import Package + + +def add(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + add packages callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + Application(architecture, config).add(args.package, args.without_dependencies) + + +def clean(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + clean caches callback + :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) + + +def dump_config(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + configuration dump callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + del args + config_dump = config.dump(architecture) + for section, values in sorted(config_dump.items()): + print(f'[{section}]') + for key, value in sorted(values.items()): + print(f'{key} = {value}') + print() + + +def rebuild(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + world rebuild callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + del args + app = Application(architecture, config) + packages = app.repository.packages() + app.update(packages) + + +def remove(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + remove packages callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + Application(architecture, config).remove(args.package) + + +def report(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + generate report callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + Application(architecture, config).report(args.target) + + +def status(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + package status callback + :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() + print(ahriman.pretty_print()) + print() + if args.package: + packages: Iterable[Tuple[Package, BuildStatus]] = sum( + [application.repository.reporter.get(base) for base in args.package], + start=[]) + else: + 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()}') + + +def sync(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + sync to remote server callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + Application(architecture, config).sync(args.target) + + +def update(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + update packages callback + :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) + + application = Application(architecture, config) + packages = application.get_updates(args.package, args.no_aur, args.no_manual, args.no_vcs, log_fn) + if args.dry_run: + return + + application.update(packages) + + +def web(args: argparse.Namespace, architecture: str, config: Configuration) -> None: + ''' + web server callback + :param args: command line args + :param architecture: repository architecture + :param config: configuration instance + ''' + del args + from ahriman.web.web import run_server, setup_service + application = setup_service(architecture, config) + run_server(application, architecture) diff --git a/src/ahriman/repository/__init__.py b/src/ahriman/core/repository/__init__.py similarity index 100% rename from src/ahriman/repository/__init__.py rename to src/ahriman/core/repository/__init__.py diff --git a/src/ahriman/repository/cleaner.py b/src/ahriman/core/repository/cleaner.py similarity index 97% rename from src/ahriman/repository/cleaner.py rename to src/ahriman/core/repository/cleaner.py index 4e9c5185..363f8c3e 100644 --- a/src/ahriman/repository/cleaner.py +++ b/src/ahriman/core/repository/cleaner.py @@ -22,7 +22,7 @@ import shutil from typing import List -from ahriman.repository.properties import Properties +from ahriman.core.repository.properties import Properties class Cleaner(Properties): diff --git a/src/ahriman/repository/executor.py b/src/ahriman/core/repository/executor.py similarity index 99% rename from src/ahriman/repository/executor.py rename to src/ahriman/core/repository/executor.py index 0b4da52b..60c14aa7 100644 --- a/src/ahriman/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -24,9 +24,9 @@ from typing import Dict, Iterable, List, Optional from ahriman.core.build_tools.task import Task from ahriman.core.report.report import Report +from ahriman.core.repository.cleaner import Cleaner from ahriman.core.upload.uploader import Uploader from ahriman.models.package import Package -from ahriman.repository.cleaner import Cleaner class Executor(Cleaner): diff --git a/src/ahriman/repository/properties.py b/src/ahriman/core/repository/properties.py similarity index 100% rename from src/ahriman/repository/properties.py rename to src/ahriman/core/repository/properties.py diff --git a/src/ahriman/repository/repository.py b/src/ahriman/core/repository/repository.py similarity index 94% rename from src/ahriman/repository/repository.py rename to src/ahriman/core/repository/repository.py index 24fb06be..c108b427 100644 --- a/src/ahriman/repository/repository.py +++ b/src/ahriman/core/repository/repository.py @@ -21,10 +21,10 @@ import os from typing import Dict, List +from ahriman.core.repository.executor import Executor +from ahriman.core.repository.update_handler import UpdateHandler from ahriman.core.util import package_like from ahriman.models.package import Package -from ahriman.repository.executor import Executor -from ahriman.repository.update_handler import UpdateHandler class Repository(Executor, UpdateHandler): diff --git a/src/ahriman/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py similarity index 98% rename from src/ahriman/repository/update_handler.py rename to src/ahriman/core/repository/update_handler.py index 94d88a77..451b32ea 100644 --- a/src/ahriman/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -21,8 +21,8 @@ import os from typing import Iterable, List +from ahriman.core.repository.cleaner import Cleaner from ahriman.models.package import Package -from ahriman.repository.cleaner import Cleaner class UpdateHandler(Cleaner): diff --git a/src/ahriman/core/watcher/client.py b/src/ahriman/core/watcher/client.py index 64899654..98e10381 100644 --- a/src/ahriman/core/watcher/client.py +++ b/src/ahriman/core/watcher/client.py @@ -19,8 +19,10 @@ # from __future__ import annotations +from typing import List, Optional, Tuple + from ahriman.core.configuration import Configuration -from ahriman.models.build_status import BuildStatusEnum +from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package @@ -36,10 +38,28 @@ class Client: :param status: current package build status ''' + # pylint: disable=R0201 + 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 + 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: basename to remove + :param base: package base to remove ''' def update(self, base: str, status: BuildStatusEnum) -> None: diff --git a/src/ahriman/core/watcher/watcher.py b/src/ahriman/core/watcher/watcher.py index 456db20a..70d3888a 100644 --- a/src/ahriman/core/watcher/watcher.py +++ b/src/ahriman/core/watcher/watcher.py @@ -24,7 +24,7 @@ import os from typing import Any, Dict, List, Optional, Tuple from ahriman.core.configuration import Configuration -from ahriman.repository.repository import Repository +from ahriman.core.repository.repository import Repository from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.package import Package @@ -73,7 +73,7 @@ class Watcher: ''' def parse_single(properties: Dict[str, Any]) -> None: package = Package.from_json(properties['package']) - status = BuildStatus(**properties['status']) + status = BuildStatus.from_json(properties['status']) if package.base in self.known: self.known[package.base] = (package, status) diff --git a/src/ahriman/core/watcher/web_client.py b/src/ahriman/core/watcher/web_client.py index a0bc210d..5e6e2d82 100644 --- a/src/ahriman/core/watcher/web_client.py +++ b/src/ahriman/core/watcher/web_client.py @@ -18,10 +18,12 @@ # 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 ahriman.models.build_status import BuildStatusEnum +from ahriman.models.build_status import BuildStatusEnum, BuildStatus from ahriman.models.package import Package @@ -50,7 +52,7 @@ class WebClient(Client): ''' 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 @@ -77,6 +79,44 @@ class WebClient(Client): except Exception: self.logger.exception(f'could not add {package.base}', exc_info=True) + 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.raise_for_status() + + status_json = response.json() + return [ + (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) + except Exception: + self.logger.exception(f'could not get {base}', exc_info=True) + 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() + + 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) + except Exception: + self.logger.exception('could not get service status', exc_info=True) + return BuildStatus() + def remove(self, base: str) -> None: ''' remove packages from watcher diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py index 4a6bb028..2d3b4963 100644 --- a/src/ahriman/models/build_status.py +++ b/src/ahriman/models/build_status.py @@ -17,10 +17,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +from __future__ import annotations + import datetime from enum import Enum -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, Type, Union + +from ahriman.core.util import pretty_datetime class BuildStatusEnum(Enum): @@ -72,6 +76,22 @@ class BuildStatus: 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')) + + def pretty_print(self) -> str: + ''' + generate pretty string representation + :return: print-friendly string + ''' + return f'{self.status.value} ({pretty_datetime(self.timestamp)})' + def view(self) -> Dict[str, Any]: ''' generate json status view @@ -81,3 +101,10 @@ class BuildStatus: 'status': self.status.value, 'timestamp': self.timestamp } + + def __repr__(self) -> str: + ''' + generate string representation of object + :return: unique string representation + ''' + 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 4ccadfd6..a6c597f7 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -57,6 +57,13 @@ class Package: ''' 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: ''' @@ -224,6 +231,14 @@ class Package: 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}' + def view(self) -> Dict[str, Any]: ''' generate json package view diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index fbe5ccd2..bd4c2dc2 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -41,7 +41,7 @@ class IndexView(BaseView): 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 diff --git a/src/ahriman/web/views/package.py b/src/ahriman/web/views/package.py index f87cbdd5..1236383d 100644 --- a/src/ahriman/web/views/package.py +++ b/src/ahriman/web/views/package.py @@ -41,10 +41,12 @@ class PackageView(BaseView): except KeyError: raise HTTPNotFound() - response = { - 'package': package.view(), - 'status': status.view() - } + response = [ + { + 'package': package.view(), + 'status': status.view() + } + ] return json_response(response) async def delete(self) -> Response: