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: