Docstring update (#58)

* migrate docstrings from reST to google format

* add raises note

Also change behaviour of the `from_option` method to fallback to
disabled instead of raising exception on unknown option

* fix part of warnings for sphinx

* make identation a bit more readable

* review fixes

* add verbose description for properties to make them parsed by sphinx extenstion

* add demo sphinx generator
This commit is contained in:
2022-04-17 20:25:28 +03:00
committed by GitHub
parent 0db619136d
commit d90f417cae
203 changed files with 5246 additions and 1636 deletions

View File

@ -43,8 +43,12 @@ SubParserAction = TypeVar("SubParserAction", bound="argparse._SubParsersAction[a
def _formatter(prog: str) -> argparse.HelpFormatter:
"""
formatter for the help message
:param prog: application name
:return: formatter used by default
Args:
prog(str): application name
Returns:
argparse.HelpFormatter: formatter used by default
"""
return argparse.ArgumentDefaultsHelpFormatter(prog, width=120)
@ -52,7 +56,9 @@ def _formatter(prog: str) -> argparse.HelpFormatter:
def _parser() -> argparse.ArgumentParser:
"""
command line parser generator
:return: command line parser for the application
Returns:
argparse.ArgumentParser: command line parser for the application
"""
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH Linux ReposItory MANager",
epilog="Argument list can also be read from file by using @ prefix.",
@ -107,8 +113,12 @@ def _parser() -> argparse.ArgumentParser:
def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for AUR search subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("aur-search", aliases=["search"], help="search for package",
description="search for package in AUR using API", formatter_class=_formatter)
@ -126,8 +136,12 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing help subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit",
@ -141,8 +155,12 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for listing unsafe commands
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter)
@ -156,8 +174,12 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for key import subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("key-import", help="import PGP key",
description="import PGP key from public sources to the repository user",
@ -175,8 +197,12 @@ def _set_key_import_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package addition subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-add", aliases=["add", "package-update"], help="add package",
description="add existing or new package to the build queue",
@ -204,8 +230,12 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package removal subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-remove", aliases=["remove"], help="remove package",
description="remove package from the repository", formatter_class=_formatter)
@ -217,8 +247,12 @@ def _set_package_remove_parser(root: SubParserAction) -> argparse.ArgumentParser
def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-status", aliases=["status"], help="get package status",
description="request status of the package",
@ -237,8 +271,12 @@ def _set_package_status_parser(root: SubParserAction) -> argparse.ArgumentParser
def _set_package_status_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status remove subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-status-remove", help="remove package status",
description="remove the package from the status page",
@ -254,8 +292,12 @@ def _set_package_status_remove_parser(root: SubParserAction) -> argparse.Argumen
def _set_package_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for package status update subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("package-status-update", aliases=["status-update"], help="update package status",
description="update package status on the status page", formatter_class=_formatter)
@ -272,8 +314,12 @@ def _set_package_status_update_parser(root: SubParserAction) -> argparse.Argumen
def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for new patch subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-add", help="add patch set", description="create or update source patches",
epilog="In order to add a patch set for the package you will need to clone "
@ -292,8 +338,12 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for list patches subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-list", help="list patch sets",
description="list available patches for the package", formatter_class=_formatter)
@ -306,8 +356,12 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove patches subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("patch-remove", help="remove patch set", description="remove patches for the package",
formatter_class=_formatter)
@ -319,8 +373,12 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository backup subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup settings and database", formatter_class=_formatter)
@ -332,8 +390,12 @@ def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository check subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-check", aliases=["check"], help="check for updates",
description="check for packages updates. Same as update --dry-run --no-manual",
@ -348,8 +410,12 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository clean subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-clean", aliases=["clean"], help="clean local caches",
description="remove local caches",
@ -368,8 +434,12 @@ def _set_repo_clean_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for config subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-config", aliases=["config"], help="dump configuration",
description="dump configuration for the specified architecture",
@ -381,8 +451,12 @@ def _set_repo_config_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository rebuild subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-rebuild", aliases=["rebuild"], help="rebuild repository",
description="force rebuild whole repository", formatter_class=_formatter)
@ -403,8 +477,12 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for remove unknown packages subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-remove-unknown", aliases=["remove-unknown"], help="remove unknown packages",
description="remove packages which are missing in AUR and do not have local PKGBUILDs",
@ -418,8 +496,12 @@ def _set_repo_remove_unknown_parser(root: SubParserAction) -> argparse.ArgumentP
def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for report subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-report", aliases=["report"], help="generate report",
description="generate repository report according to current settings",
@ -433,8 +515,12 @@ def _set_repo_report_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository restore subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-restore", help="restore repository data",
description="restore settings and database", formatter_class=_formatter)
@ -447,8 +533,12 @@ def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for setup subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-setup", aliases=["init", "repo-init", "setup"], help="initial service configuration",
description="create initial service configuration, requires root",
@ -472,8 +562,12 @@ def _set_repo_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for sign subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sign", aliases=["sign"], help="sign packages",
description="(re-)sign packages and repository database according to current settings",
@ -487,8 +581,12 @@ def _set_repo_sign_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository status update subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-status-update", help="update repository status",
description="update repository status on the status page", formatter_class=_formatter)
@ -502,8 +600,12 @@ def _set_repo_status_update_parser(root: SubParserAction) -> argparse.ArgumentPa
def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository sync subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-sync", aliases=["sync"], help="sync repository",
description="sync repository files to remote server according to current settings",
@ -517,8 +619,12 @@ def _set_repo_sync_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repository update subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("repo-update", aliases=["update"], help="update packages",
description="check for packages updates and run build process if requested",
@ -537,8 +643,12 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for create user subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("user-add", help="create or update user",
description="update user for web services with the given password and role. "
@ -559,8 +669,12 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user list subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("user-list", help="user known users and their access",
description="list users from the user mapping and their roles",
@ -576,8 +690,12 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for user removal subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("user-remove", help="remove user",
description="remove user from the user mapping and update the configuration",
@ -592,8 +710,12 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web subcommand
:param root: subparsers for the commands
:return: created argument parser
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("web", help="web server", description="start web server", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Web, lock=None, no_report=True, parser=_parser)

View File

@ -32,7 +32,9 @@ class Application(Packages, Repository):
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
:param result: build result
Args:
result(Result): build result
"""
self.report([], result)
self.sync([], result.success)
@ -40,7 +42,9 @@ class Application(Packages, Repository):
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
Returns:
Set[str]: list of known packages
"""
known_packages: Set[str] = set()
# local set

View File

@ -39,21 +39,33 @@ class Packages(Properties):
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
:param result: build result
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _known_packages(self) -> Set[str]:
"""
load packages from repository and pacman repositories
:return: list of known packages
Returns:
Set[str]: list of known packages
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def _add_archive(self, source: str, *_: Any) -> None:
"""
add package from archive
:param source: path to package archive
Args:
source(str): path to package archive
"""
local_path = Path(source)
dst = self.repository.paths.packages / local_path.name
@ -62,9 +74,11 @@ class Packages(Properties):
def _add_aur(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
"""
add package from AUR
:param source: package base name
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
Args:
source(str): package base name
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
package = Package.load(source, PackageSource.AUR, self.repository.pacman, self.repository.aur_url)
@ -77,7 +91,9 @@ class Packages(Properties):
def _add_directory(self, source: str, *_: Any) -> None:
"""
add packages from directory
:param source: path to local directory
Args:
source(str): path to local directory
"""
local_path = Path(source)
for full_path in filter(package_like, local_path.iterdir()):
@ -86,9 +102,11 @@ class Packages(Properties):
def _add_local(self, source: str, known_packages: Set[str], without_dependencies: bool) -> None:
"""
add package from local PKGBUILDs
:param source: path to directory with local source files
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
Args:
source(str): path to directory with local source files
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
package = Package.load(source, PackageSource.Local, self.repository.pacman, self.repository.aur_url)
cache_dir = self.repository.paths.cache_for(package.base)
@ -102,7 +120,9 @@ class Packages(Properties):
def _add_remote(self, source: str, *_: Any) -> None:
"""
add package from remote sources (e.g. HTTP)
:param remote_url: remote URL to the package archive
Args:
source(str): remote URL of the package archive
"""
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True)
@ -115,9 +135,11 @@ class Packages(Properties):
def _process_dependencies(self, local_path: Path, known_packages: Set[str], without_dependencies: bool) -> None:
"""
process package dependencies
:param local_path: path to local package sources (i.e. cloned AUR repository)
:param known_packages: list of packages which are known by the service
:param without_dependencies: if set, dependency check will be disabled
Args:
local_path(Path): path to local package sources (i.e. cloned AUR repository)
known_packages(Set[str]): list of packages which are known by the service
without_dependencies(bool): if set, dependency check will be disabled
"""
if without_dependencies:
return
@ -128,9 +150,11 @@ class Packages(Properties):
def add(self, names: Iterable[str], source: PackageSource, without_dependencies: bool) -> None:
"""
add packages for the next build
:param names: list of package bases to add
:param source: package source to add
:param without_dependencies: if set, dependency check will be disabled
Args:
names(Iterable[str]): list of package bases to add
source(PackageSource): package source to add
without_dependencies(bool): if set, dependency check will be disabled
"""
known_packages = self._known_packages() # speedup dependencies processing
@ -142,7 +166,9 @@ class Packages(Properties):
def remove(self, names: Iterable[str]) -> None:
"""
remove packages from repository
:param names: list of packages (either base or name) to remove
Args:
names(Iterable[str]): list of packages (either base or name) to remove
"""
self.repository.process_remove(names)
self._finalize(Result())

View File

@ -27,20 +27,24 @@ from ahriman.core.repository import Repository
class Properties:
"""
application base properties class
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar database: database instance
:ivar logger: application logger
:ivar repository: repository instance
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
database(SQLite): database instance
logger(logging.Logger): application logger
repository(Repository): repository instance
"""
def __init__(self, architecture: str, configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
self.logger = logging.getLogger("root")
self.configuration = configuration

View File

@ -38,17 +38,24 @@ class Repository(Properties):
def _finalize(self, result: Result) -> None:
"""
generate report and sync to remote server
:param result: build result
Args:
result(Result): build result
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def clean(self, cache: bool, chroot: bool, manual: bool, packages: bool) -> None:
"""
run all clean methods. Warning: some functions might not be available under non-root
:param cache: clear directory with package caches
:param chroot: clear build chroot
:param manual: clear directory with manually added packages
:param packages: clear directory with built packages
Args:
cache(bool): clear directory with package caches
chroot(bool): clear build chroot
manual(bool): clear directory with manually added packages
packages(bool): clear directory with built packages
"""
if cache:
self.repository.clear_cache()
@ -62,8 +69,10 @@ class Repository(Properties):
def report(self, target: Iterable[str], result: Result) -> None:
"""
generate report
:param target: list of targets to run (e.g. html)
:param result: build result
Args:
target(Iterable[str]): list of targets to run (e.g. html)
result(Result): build result
"""
targets = target or None
self.repository.process_report(targets, result)
@ -71,7 +80,9 @@ class Repository(Properties):
def sign(self, packages: Iterable[str]) -> None:
"""
sign packages and repository
:param packages: only sign specified packages
Args:
packages(Iterable[str]): only sign specified packages
"""
# copy to prebuilt directory
for package in self.repository.packages():
@ -94,8 +105,10 @@ class Repository(Properties):
def sync(self, target: Iterable[str], built_packages: Iterable[Package]) -> None:
"""
sync to remote server
:param target: list of targets to run (e.g. s3)
:param built_packages: list of packages which has just been built
Args:
target(Iterable[str]): list of targets to run (e.g. s3)
built_packages(Iterable[Package]): list of packages which has just been built
"""
targets = target or None
self.repository.process_sync(targets, built_packages)
@ -103,7 +116,9 @@ class Repository(Properties):
def unknown(self) -> List[str]:
"""
get packages which were not found in AUR
:return: unknown package archive list
Returns:
List[str]: unknown package archive list
"""
def has_local(probe: Package) -> bool:
cache_dir = self.repository.paths.cache_for(probe.base)
@ -135,7 +150,12 @@ class Repository(Properties):
def update(self, updates: Iterable[Package]) -> Result:
"""
run package updates
:param updates: list of packages to update
Args:
updates(Iterable[Package]): list of packages to update
Returns:
Result: update result
"""
def process_update(paths: Iterable[Path], result: Result) -> None:
if not paths:
@ -162,13 +182,17 @@ class Repository(Properties):
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
:param no_local: do not check local packages for updates
:param no_manual: do not check for manual updates
:param no_vcs: do not check VCS packages
:param log_fn: logger function to log updates
:return: list of out-of-dated packages
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
no_aur(bool): do not check for aur updates
no_local(bool): do not check local packages for updates
no_manual(bool): do not check for manual updates
no_vcs(bool): do not check VCS packages
log_fn(Callable[[str], None]): logger function to log updates
Returns:
List[Package]: list of out-of-dated packages
"""
updates = {}

View File

@ -36,11 +36,13 @@ class Add(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
application.add(args.package, args.source, args.without_dependencies)

View File

@ -41,11 +41,13 @@ class Backup(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
backup_paths = Backup.get_paths(configuration)
with TarFile(args.path, mode="w") as archive: # well we don't actually use compression
@ -56,8 +58,12 @@ class Backup(Handler):
def get_paths(configuration: Configuration) -> Set[Path]:
"""
extract paths to backup
:param configuration: configuration instance
:return: map of the filesystem paths
Args:
configuration(Configuration): configuration instance
Returns:
Set[Path]: map of the filesystem paths
"""
paths = set(configuration.include.glob("*.ini"))

View File

@ -36,11 +36,13 @@ class Clean(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).clean(
args.cache, args.chroot, args.manual, args.packages)

View File

@ -38,11 +38,13 @@ class Dump(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
dump = configuration.dump()
for section, values in sorted(dump.items()):

View File

@ -34,8 +34,10 @@ from ahriman.models.repository_paths import RepositoryPaths
class Handler:
"""
base handler class for command callbacks
:cvar ALLOW_AUTO_ARCHITECTURE_RUN: allow to define architecture from existing repositories
:cvar ALLOW_MULTI_ARCHITECTURE_RUN: allow to run with multiple architectures
Attributes:
ALLOW_AUTO_ARCHITECTURE_RUN(bool): (class attribute) allow defining architecture from existing repositories
ALLOW_MULTI_ARCHITECTURE_RUN(bool): (class attribute) allow running with multiple architectures
"""
ALLOW_AUTO_ARCHITECTURE_RUN = True
@ -45,8 +47,15 @@ class Handler:
def architectures_extract(cls: Type[Handler], args: argparse.Namespace) -> List[str]:
"""
get known architectures
:param args: command line args
:return: list of architectures for which tree is created
Args:
args(argparse.Namespace): command line args
Returns:
List[str]: list of architectures for which tree is created
Raises:
MissingArchitecture: if no architecture set and automatic detection is not allowed or failed
"""
if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None:
# for some parsers (e.g. config) we need to run with specific architecture
@ -69,9 +78,13 @@ class Handler:
def call(cls: Type[Handler], args: argparse.Namespace, architecture: str) -> bool:
"""
additional function to wrap all calls for multiprocessing library
:param args: command line args
:param architecture: repository architecture
:return: True on success, False otherwise
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
Returns:
bool: True on success, False otherwise
"""
try:
configuration = Configuration.from_path(args.configuration, architecture, args.quiet)
@ -89,8 +102,15 @@ class Handler:
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
Args:
args(argparse.Namespace): command line args
Returns:
int: 0 on success, 1 otherwise
Raises:
MultipleArchitectures: if more than one architecture supplied and no multi architecture supported
"""
architectures = cls.architectures_extract(args)
@ -112,11 +132,16 @@ class Handler:
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
@ -124,8 +149,13 @@ class Handler:
def check_if_empty(enabled: bool, predicate: bool) -> None:
"""
check condition and flag and raise ExitCode exception in case if it is enabled and condition match
:param enabled: if False no check will be performed
:param predicate: indicates condition on which exception should be thrown
Args:
enabled(bool): if False no check will be performed
predicate(bool): indicates condition on which exception should be thrown
Raises:
ExitCode: if result is empty and check is enabled
"""
if enabled and predicate:
raise ExitCode()

View File

@ -37,11 +37,13 @@ class Help(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
parser: argparse.ArgumentParser = args.parser()
if args.command is None:

View File

@ -38,11 +38,13 @@ class KeyImport(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).repository.sign.key_import(
args.key_server, args.key)

View File

@ -42,11 +42,13 @@ class Patch(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
@ -61,9 +63,11 @@ class Patch(Handler):
def patch_set_create(application: Application, sources_dir: str, track: List[str]) -> None:
"""
create patch set for the package base
:param application: application instance
:param sources_dir: path to directory with the package sources
:param track: track files which match the glob before creating the patch
Args:
application(Application): application instance
sources_dir(str): path to directory with the package sources
track(List[str]): track files which match the glob before creating the patch
"""
package = Package.load(sources_dir, PackageSource.Local, application.repository.pacman,
application.repository.aur_url)
@ -74,9 +78,11 @@ class Patch(Handler):
def patch_set_list(application: Application, package_base: Optional[str], exit_code: bool) -> None:
"""
list patches available for the package base
:param application: application instance
:param package_base: package base
:param exit_code: raise ExitCode on empty search result
Args:
application(Application): application instance
package_base(Optional[str]): package base
exit_code(bool): exit with error on empty search result
"""
patches = application.database.patches_list(package_base)
Patch.check_if_empty(exit_code, not patches)
@ -89,7 +95,9 @@ class Patch(Handler):
def patch_set_remove(application: Application, package_base: str) -> None:
"""
remove patch set for the package base
:param application: application instance
:param package_base: package base
Args:
application(Application): application instance
package_base(str): package base
"""
application.database.patches_remove(package_base)

View File

@ -38,11 +38,13 @@ class Rebuild(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
depends_on = set(args.depends_on) if args.depends_on else None
@ -65,7 +67,11 @@ class Rebuild(Handler):
def extract_packages(application: Application) -> List[Package]:
"""
extract packages from database file
:param application: application instance
:return: list of packages which were stored in database
Args:
application(Application): application instance
Returns:
List[Package]: list of packages which were stored in database
"""
return [package for (package, _) in application.database.packages_get()]

View File

@ -36,10 +36,12 @@ class Remove(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).remove(args.package)

View File

@ -37,11 +37,13 @@ class RemoveUnknown(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
unknown_packages = application.unknown()

View File

@ -37,10 +37,12 @@ class Report(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).report(args.target, Result())

View File

@ -38,11 +38,13 @@ class Restore(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
with TarFile(args.path) as archive:
archive.extractall(path=args.output)

View File

@ -34,7 +34,9 @@ from ahriman.models.aur_package import AURPackage
class Search(Handler):
"""
packages search handler
:cvar SORT_FIELDS: allowed fields to sort the package list
Attributes:
SORT_FIELDS(Set[str]): (class attribute) allowed fields to sort the package list
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False # it should be called only as "no-architecture"
@ -45,11 +47,13 @@ class Search(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
official_packages_list = Official.multisearch(*args.search)
aur_packages_list = AUR.multisearch(*args.search)
@ -64,9 +68,16 @@ class Search(Handler):
def sort(packages: Iterable[AURPackage], sort_by: str) -> List[AURPackage]:
"""
sort package list by specified field
:param packages: packages list to sort
:param sort_by: AUR package field name to sort by
:return: sorted list for packages
Args:
packages(Iterable[AURPackage]): packages list to sort
sort_by(str): AUR package field name to sort by
Returns:
List[AURPackage]: sorted list for packages
Raises:
InvalidOption: if search fields is not in list of allowed ones
"""
if sort_by not in Search.SORT_FIELDS:
raise InvalidOption(sort_by)

View File

@ -31,10 +31,12 @@ from ahriman.models.repository_paths import RepositoryPaths
class Setup(Handler):
"""
setup handler
:cvar ARCHBUILD_COMMAND_PATH: default devtools command
:cvar BIN_DIR_PATH: directory for custom binaries
:cvar MIRRORLIST_PATH: path to pacman default mirrorlist (used by multilib repository)
:cvar SUDOERS_PATH: path to sudoers.d include configuration
Attributes:
ARCHBUILD_COMMAND_PATH(Path): (class attribute) default devtools command
BIN_DIR_PATH(Path): (class attribute) directory for custom binaries
MIRRORLIST_PATH(Path): (class attribute) path to pacman default mirrorlist (used by multilib repository)
SUDOERS_PATH(Path): (class attribute) path to sudoers.d include configuration
"""
ALLOW_AUTO_ARCHITECTURE_RUN = False
@ -49,11 +51,13 @@ class Setup(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Setup.configuration_create_ahriman(args, architecture, args.repository, configuration.include)
configuration.reload()
@ -72,9 +76,13 @@ class Setup(Handler):
def build_command(prefix: str, architecture: str) -> Path:
"""
generate build command name
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:return: valid devtools command name
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
Returns:
Path: valid devtools command name
"""
return Setup.BIN_DIR_PATH / f"{prefix}-{architecture}-build"
@ -83,10 +91,12 @@ class Setup(Handler):
include_path: Path) -> None:
"""
create service specific configuration
:param args: command line args
:param architecture: repository architecture
:param repository: repository name
:param include_path: path to directory with configuration includes
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
repository(str): repository name
include_path(Path): path to directory with configuration includes
"""
configuration = Configuration()
@ -114,12 +124,14 @@ class Setup(Handler):
no_multilib: bool, repository: str, paths: RepositoryPaths) -> None:
"""
create configuration for devtools based on `source` configuration
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
:param source: path to source configuration file
:param no_multilib: do not add multilib repository
:param repository: repository name
:param paths: repository paths instance
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
source(Path): path to source configuration file
no_multilib(bool): do not add multilib repository
repository(str): repository name
paths(RepositoryPaths): repository paths instance
"""
configuration = Configuration()
# preserve case
@ -149,8 +161,10 @@ class Setup(Handler):
def configuration_create_makepkg(packager: str, paths: RepositoryPaths) -> None:
"""
create configuration for makepkg
:param packager: packager identifier (e.g. name, email)
:param paths: repository paths instance
Args:
packager(str): packager identifier (e.g. name, email)
paths(RepositoryPaths): repository paths instance
"""
(paths.root / ".makepkg.conf").write_text(f"PACKAGER='{packager}'\n", encoding="utf8")
@ -158,8 +172,10 @@ class Setup(Handler):
def configuration_create_sudo(prefix: str, architecture: str) -> None:
"""
create configuration to run build command with sudo without password
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
"""
command = Setup.build_command(prefix, architecture)
Setup.SUDOERS_PATH.write_text(f"ahriman ALL=(ALL) NOPASSWD: {command} *\n", encoding="utf8")
@ -169,8 +185,10 @@ class Setup(Handler):
def executable_create(prefix: str, architecture: str) -> None:
"""
create executable for the service
:param prefix: command prefix in {prefix}-{architecture}-build
:param architecture: repository architecture
Args:
prefix(str): command prefix in {prefix}-{architecture}-build
architecture(str): repository architecture
"""
command = Setup.build_command(prefix, architecture)
command.unlink(missing_ok=True)

View File

@ -36,10 +36,12 @@ class Sign(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).sign(args.package)

View File

@ -42,11 +42,13 @@ class Status(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
# we are using reporter here
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter

View File

@ -39,11 +39,13 @@ class StatusUpdate(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
# we are using reporter here
client = Application(architecture, configuration, no_report=False, unsafe=unsafe).repository.reporter

View File

@ -28,7 +28,7 @@ from ahriman.core.configuration import Configuration
class Sync(Handler):
"""
remove sync handler
remote sync handler
"""
@classmethod
@ -36,10 +36,12 @@ class Sync(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
Application(architecture, configuration, no_report, unsafe).sync(args.target, [])

View File

@ -24,7 +24,6 @@ from typing import List, Type
from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
from ahriman.core.formatters.string_printer import StringPrinter
@ -40,11 +39,13 @@ class UnsafeCommands(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
@ -58,20 +59,25 @@ class UnsafeCommands(Handler):
def check_unsafe(command: str, unsafe_commands: List[str], parser: argparse.ArgumentParser) -> None:
"""
check if command is unsafe
:param command: command to check
:param unsafe_commands: list of unsafe commands
:param parser: generated argument parser
Args:
command(str): command to check
unsafe_commands(List[str]): list of unsafe commands
parser(argparse.ArgumentParser): generated argument parser
"""
args = parser.parse_args(shlex.split(command))
if args.command in unsafe_commands:
raise ExitCode()
UnsafeCommands.check_if_empty(True, args.command in unsafe_commands)
@staticmethod
def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]:
"""
extract unsafe commands from argument parser
:param parser: generated argument parser
:return: list of commands with default unsafe flag
Args:
parser(argparse.ArgumentParser): generated argument parser
Returns:
List[str]: list of commands with default unsafe flag
"""
# pylint: disable=protected-access
subparser = next(action for action in parser._actions if isinstance(action, argparse._SubParsersAction))

View File

@ -36,11 +36,13 @@ class Update(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
application = Application(architecture, configuration, no_report, unsafe)
packages = application.updates(args.package, args.no_aur, args.no_local, args.no_manual, args.no_vcs,
@ -56,9 +58,13 @@ class Update(Handler):
def log_fn(application: Application, dry_run: bool) -> Callable[[str], None]:
"""
package updates log function
:param application: application instance
:param dry_run: do not perform update itself
:return: in case if dry_run is set it will return print, logger otherwise
Args:
application(Application): application instance
dry_run(bool): do not perform update itself
Returns:
Callable[[str], None]: in case if dry_run is set it will return print, logger otherwise
"""
def inner(line: str) -> None:
return print(line) if dry_run else application.logger.info(line)

View File

@ -43,11 +43,13 @@ class User(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
database = SQLite.load(configuration)
@ -72,11 +74,13 @@ class User(Handler):
as_service_user: bool, secure: bool) -> None:
"""
enable configuration if it has been disabled
:param configuration: configuration instance
:param user: user descriptor
:param salt: password hash salt
:param as_service_user: add user as service user, also set password and user to configuration
:param secure: if true then set file permissions to 0o600
Args:
configuration(Configuration): configuration instance
user(MUser): user descriptor
salt(str): password hash salt
as_service_user(bool): add user as service user, also set password and user to configuration
secure(bool): if true then set file permissions to 0o600
"""
configuration.set_option("auth", "salt", salt)
if as_service_user:
@ -88,8 +92,12 @@ class User(Handler):
def configuration_get(include_path: Path) -> Configuration:
"""
create configuration instance
:param include_path: path to directory with configuration includes
:return: configuration instance. In case if there are local settings they will be loaded
Args:
include_path(Path): path to directory with configuration includes
Returns:
Configuration: configuration instance. In case if there are local settings they will be loaded
"""
target = include_path / "auth.ini"
configuration = Configuration()
@ -103,8 +111,10 @@ class User(Handler):
def configuration_write(configuration: Configuration, secure: bool) -> None:
"""
write configuration file
:param configuration: configuration instance
:param secure: if true then set file permissions to 0o600
Args:
configuration(Configuration): configuration instance
secure(bool): if true then set file permissions to 0o600
"""
path, _ = configuration.check_loaded()
with path.open("w") as ahriman_configuration:
@ -116,9 +126,13 @@ class User(Handler):
def get_salt(configuration: Configuration, salt_length: int = 20) -> str:
"""
get salt from configuration or create new string
:param configuration: configuration instance
:param salt_length: salt length
:return: current salt
Args:
configuration(Configuration): configuration instance
salt_length(int, optional): salt length (Default value = 20)
Returns:
str: current salt
"""
if salt := configuration.get("auth", "salt", fallback=None):
return salt
@ -128,8 +142,12 @@ class User(Handler):
def user_create(args: argparse.Namespace) -> MUser:
"""
create user descriptor from arguments
:param args: command line args
:return: built user descriptor
Args:
args(argparse.Namespace): command line args
Returns:
MUser: built user descriptor
"""
user = MUser(args.username, args.password, args.role)
if user.password is None:

View File

@ -39,11 +39,13 @@ class Web(Handler):
configuration: Configuration, no_report: bool, unsafe: bool) -> None:
"""
callback for command line
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
# we are using local import for optional dependencies
from ahriman.web.web import run_server, setup_service

View File

@ -37,19 +37,23 @@ 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 paths: repository paths instance
:ivar unsafe: skip user check
Attributes:
force(bool): remove lock file on start if any
path(Path): path to lock file if any
reporter(Client): build status reporter instance
paths(RepositoryPaths): repository paths instance
unsafe(bool): skip user check
"""
def __init__(self, args: argparse.Namespace, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param args: command line args
:param architecture: repository architecture
:param configuration: configuration instance
Args:
args(argparse.Namespace): command line args
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.path = Path(f"{args.lock}_{architecture}") if args.lock is not None else None
self.force = args.force
@ -62,11 +66,11 @@ class Lock:
"""
default workflow is the following:
check user UID
check if there is lock file
check web status watcher status
create lock file
report to web if enabled
1. Check user UID
2. Check if there is lock file
3. Check web status watcher status
4. Create lock file
5. Report to status page if enabled
"""
self.check_user()
self.check_version()
@ -78,10 +82,14 @@ class Lock:
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)
Args:
exc_type(Optional[Type[Exception]]): exception type name if any
exc_val(Optional[Exception]): exception raised if any
exc_tb(TracebackType): exception traceback if any
Returns:
Literal[False]: always False (do not suppress any exception)
"""
self.clear()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
@ -116,6 +124,9 @@ class Lock:
def create(self) -> None:
"""
create lock file
Raises:
DuplicateRun: if lock exists and no force flag supplied
"""
if self.path is None:
return

View File

@ -26,13 +26,17 @@ from ahriman.core.configuration import Configuration
class Pacman:
"""
alpm wrapper
:ivar handle: pyalpm root `Handle`
Attributes:
handle(Handle): pyalpm root `Handle`
"""
def __init__(self, configuration: Configuration) -> None:
"""
default constructor
:param configuration: configuration instance
Args:
configuration(Configuration): configuration instance
"""
root = configuration.get("alpm", "root")
pacman_root = configuration.getpath("alpm", "database")
@ -43,7 +47,9 @@ class Pacman:
def all_packages(self) -> Set[str]:
"""
get list of packages known for alpm
:return: list of package names
Returns:
Set[str]: list of package names
"""
result: Set[str] = set()
for database in self.handle.get_syncdbs():

View File

@ -30,10 +30,12 @@ from ahriman.models.aur_package import AURPackage
class AUR(Remote):
"""
AUR RPC wrapper
:cvar DEFAULT_RPC_URL: default AUR RPC url
:cvar DEFAULT_RPC_VERSION: default AUR RPC version
:ivar rpc_url: AUR RPC url
:ivar rpc_version: AUR RPC version
Attributes:
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
rpc_url(str): AUR RPC url
rpc_version(str): AUR RPC version
"""
DEFAULT_RPC_URL = "https://aur.archlinux.org/rpc"
@ -42,8 +44,10 @@ class AUR(Remote):
def __init__(self, rpc_url: Optional[str] = None, rpc_version: Optional[str] = None) -> None:
"""
default constructor
:param rpc_url: AUR RPC url
:param rpc_version: AUR RPC version
Args:
rpc_url(Optional[str], optional): AUR RPC url (Default value = None)
rpc_version(Optional[str], optional): AUR RPC version (Default value = None)
"""
Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
@ -53,8 +57,15 @@ class AUR(Remote):
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
"""
parse RPC response to package list
:param response: RPC response json
:return: list of parsed packages
Args:
response(Dict[str, Any]): RPC response json
Returns:
List[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
"""
response_type = response["type"]
if response_type == "error":
@ -65,10 +76,14 @@ class AUR(Remote):
def make_request(self, request_type: str, *args: str, **kwargs: str) -> List[AURPackage]:
"""
perform request to AUR RPC
:param request_type: AUR request type, e.g. search, info
:param args: list of arguments to be passed as args query parameter
:param kwargs: list of additional named parameters like by
:return: response parsed to package list
Args:
request_type(str): AUR request type, e.g. search, info
*args(str): list of arguments to be passed as args query parameter
**kwargs(str): list of additional named parameters like by
Returns:
List[AURPackage]: response parsed to package list
"""
query: Dict[str, Any] = {
"type": request_type,
@ -98,8 +113,12 @@ class AUR(Remote):
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
"""
packages = self.make_request("info", package_name)
return next(package for package in packages if package.name == package_name)
@ -107,7 +126,11 @@ class AUR(Remote):
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
Args:
*keywords(str): keywords to search
Returns:
List[AURPackage]: list of packages which match the criteria
"""
return self.make_request("search", *keywords, by="name-desc")

View File

@ -30,8 +30,10 @@ from ahriman.models.aur_package import AURPackage
class Official(Remote):
"""
official repository RPC wrapper
:cvar DEFAULT_RPC_URL: default AUR RPC url
:ivar rpc_url: AUR RPC url
Attributes:
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
rpc_url(str): AUR RPC url
"""
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
@ -39,7 +41,9 @@ class Official(Remote):
def __init__(self, rpc_url: Optional[str] = None) -> None:
"""
default constructor
:param rpc_url: AUR RPC url
Args:
rpc_url(Optional[str], optional): AUR RPC url (Default value = None)
"""
Remote.__init__(self)
self.rpc_url = rpc_url or self.DEFAULT_RPC_URL
@ -48,8 +52,15 @@ class Official(Remote):
def parse_response(response: Dict[str, Any]) -> List[AURPackage]:
"""
parse RPC response to package list
:param response: RPC response json
:return: list of parsed packages
Args:
response(Dict[str, Any]): RPC response json
Returns:
List[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
"""
if not response["valid"]:
raise InvalidPackageInfo("API validation error")
@ -58,9 +69,13 @@ class Official(Remote):
def make_request(self, *args: str, by: str) -> List[AURPackage]:
"""
perform request to official repositories RPC
:param args: list of arguments to be passed as args query parameter
:param by: search by the field
:return: response parsed to package list
Args:
*args(str): list of arguments to be passed as args query parameter
by(str): search by the field
Returns:
List[AURPackage]: response parsed to package list
"""
try:
response = requests.get(self.rpc_url, params={by: args})
@ -76,8 +91,12 @@ class Official(Remote):
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
"""
packages = self.make_request(package_name, by="name")
return next(package for package in packages if package.name == package_name)
@ -85,7 +104,11 @@ class Official(Remote):
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
Args:
*keywords(str): keywords to search
Returns:
List[AURPackage]: list of packages which match the criteria
"""
return self.make_request(*keywords, by="q")

View File

@ -29,7 +29,9 @@ from ahriman.models.aur_package import AURPackage
class Remote:
"""
base class for remote package search
:ivar logger: class logger
Attributes:
logger(logging.Logger): class logger
"""
def __init__(self) -> None:
@ -42,8 +44,12 @@ class Remote:
def info(cls: Type[Remote], package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
"""
return cls().package_info(package_name)
@ -52,8 +58,12 @@ class Remote:
"""
search in remote repository by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition, short words will be dropped
:param keywords: search terms, e.g. "ahriman", "is", "cool"
:return: list of packages each of them matches all search terms
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
Returns:
List[AURPackage]: list of packages each of them matches all search terms
"""
instance = cls()
packages: Dict[str, AURPackage] = {}
@ -70,23 +80,41 @@ class Remote:
def search(cls: Type[Remote], *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
Returns:
List[AURPackage]: list of packages which match the criteria
"""
return cls().package_search(*keywords)
def package_info(self, package_name: str) -> AURPackage:
"""
get package info by its name
:param package_name: package name to search
:return: package which match the package name
Args:
package_name(str): package name to search
Returns:
AURPackage: package which match the package name
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def package_search(self, *keywords: str) -> List[AURPackage]:
"""
search package in AUR web
:param keywords: keywords to search
:return: list of packages which match the criteria
Args:
*keywords(str): keywords to search
Returns:
List[AURPackage]: list of packages which match the criteria
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError

View File

@ -30,11 +30,13 @@ 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
:ivar uid: uid of the repository owner user
Attributes:
logger(logging.Logger): class logger
name(str): repository name
paths(RepositoryPaths): repository paths instance
sign_args(List[str]): additional args which have to be used to sign repository archive
uid(int): uid of the repository owner user
"""
_check_output = check_output
@ -42,9 +44,11 @@ class Repo:
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
Args:
name(str): repository name
paths(RepositoryPaths): repository paths instance
sign_args(List[str]): additional args which have to be used to sign repository archive
"""
self.logger = logging.getLogger("build_details")
self.name = name
@ -55,14 +59,19 @@ class Repo:
@property
def repo_path(self) -> Path:
"""
:return: path to repository database
get full path to the repository database
Returns:
Path: path to repository database
"""
return self.paths.repository / f"{self.name}.db.tar.gz"
def add(self, path: Path) -> None:
"""
add new package to repository
:param path: path to archive to add
Args:
path(Path): path to archive to add
"""
Repo._check_output(
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
@ -85,8 +94,10 @@ class Repo:
def remove(self, package: str, filename: Path) -> None:
"""
remove package from repository
:param package: package name to remove
:param filename: package filename to remove
Args:
package(str): package name to remove
filename(Path): package filename to remove
"""
# remove package and signature (if any) from filesystem
for full_path in self.paths.repository.glob(f"{filename}*"):

View File

@ -32,16 +32,21 @@ from ahriman.models.user_access import UserAccess
class Auth:
"""
helper to deal with user authorization
:ivar enabled: indicates if authorization is enabled
:ivar max_age: session age in seconds. It will be used for both client side and server side checks
:ivar safe_build_status: allow read only access to the index page
Attributes:
enabled(bool): indicates if authorization is enabled
logger(logging.Logger): class logger
max_age(int): session age in seconds. It will be used for both client side and server side checks
safe_build_status(bool): allow read only access to the index page
"""
def __init__(self, configuration: Configuration, provider: AuthSettings = AuthSettings.Disabled) -> None:
"""
default constructor
:param configuration: configuration instance
:param provider: authorization type definition
Args:
configuration(Configuration): configuration instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Disabled)
"""
self.logger = logging.getLogger("http")
@ -57,7 +62,9 @@ class Auth:
In case of internal authentication it must provide an interface (modal form) to login with button sends POST
request. But for an external providers behaviour can be different: e.g. OAuth provider requires sending GET
request to external resource
:return: login control as html code to insert
Returns:
str: login control as html code to insert
"""
return """<button type="button" class="btn btn-link" data-bs-toggle="modal" data-bs-target="#loginForm" style="text-decoration: none">login</button>"""
@ -65,9 +72,13 @@ class Auth:
def load(cls: Type[Auth], configuration: Configuration, database: SQLite) -> Auth:
"""
load authorization module from settings
:param configuration: configuration instance
:param database: database instance
:return: authorization module according to current settings
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
Returns:
Auth: authorization module according to current settings
"""
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
if provider == AuthSettings.Configuration:
@ -81,9 +92,13 @@ class Auth:
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
validate user password
:param username: username
:param password: entered password
:return: True in case if password matches, False otherwise
Args:
username(Optional[str]): username
password(Optional[str]): entered password
Returns:
bool: True in case if password matches, False otherwise
"""
del username, password
return True
@ -91,8 +106,12 @@ class Auth:
async def known_username(self, username: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
check if user is known
:param username: username
:return: True in case if user is known and can be authorized and False otherwise
Args:
username(Optional[str]): username
Returns:
bool: True in case if user is known and can be authorized and False otherwise
"""
del username
return True
@ -100,10 +119,14 @@ class Auth:
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool: # pylint: disable=no-self-use
"""
validate if user has access to requested resource
:param username: username
:param required: required access level
:param context: URI request path
:return: True in case if user is allowed to do this request and False otherwise
Args:
username(str): username
required(UserAccess): required access level
context(Optional[str]): URI request path
Returns:
bool: True in case if user is allowed to do this request and False otherwise
"""
del username, required, context
return True

View File

@ -29,8 +29,12 @@ except ImportError:
async def authorized_userid(*args: Any) -> Any:
"""
handle aiohttp security methods
:param args: argument list as provided by authorized_userid function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by authorized_userid function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.authorized_userid(*args) # pylint: disable=no-value-for-parameter
@ -40,8 +44,12 @@ async def authorized_userid(*args: Any) -> Any:
async def check_authorized(*args: Any) -> Any:
"""
handle aiohttp security methods
:param args: argument list as provided by check_authorized function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by check_authorized function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.check_authorized(*args) # pylint: disable=no-value-for-parameter
@ -51,8 +59,12 @@ async def check_authorized(*args: Any) -> Any:
async def forget(*args: Any) -> Any:
"""
handle aiohttp security methods
:param args: argument list as provided by forget function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by forget function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.forget(*args) # pylint: disable=no-value-for-parameter
@ -62,8 +74,12 @@ async def forget(*args: Any) -> Any:
async def remember(*args: Any) -> Any:
"""
handle disabled auth
:param args: argument list as provided by remember function
:return: None in case if no aiohttp_security module found and function call otherwise
Args:
*args(Any): argument list as provided by remember function
Returns:
Any: None in case if no aiohttp_security module found and function call otherwise
"""
if _has_aiohttp_security:
return await aiohttp_security.remember(*args) # pylint: disable=no-value-for-parameter

View File

@ -31,17 +31,21 @@ from ahriman.models.user_access import UserAccess
class Mapping(Auth):
"""
user authorization based on mapping from configuration file
:ivar salt: random generated string to salt passwords
:ivar database: database instance
Attributes:
salt(str): random generated string to salt passwords
database(SQLite): database instance
"""
def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.Configuration) -> None:
"""
default constructor
:param configuration: configuration instance
:param database: database instance
:param provider: authorization type definition
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.Configuration)
"""
Auth.__init__(self, configuration, provider)
self.database = database
@ -50,9 +54,13 @@ class Mapping(Auth):
async def check_credentials(self, username: Optional[str], password: Optional[str]) -> bool:
"""
validate user password
:param username: username
:param password: entered password
:return: True in case if password matches, False otherwise
Args:
username(Optional[str]): username
password(Optional[str]): entered password
Returns:
bool: True in case if password matches, False otherwise
"""
if username is None or password is None:
return False # invalid data supplied
@ -62,26 +70,38 @@ class Mapping(Auth):
def get_user(self, username: str) -> Optional[User]:
"""
retrieve user from in-memory mapping
:param username: username
:return: user descriptor if username is known and None otherwise
Args:
username(str): username
Returns:
Optional[User]: user descriptor if username is known and None otherwise
"""
return self.database.user_get(username)
async def known_username(self, username: Optional[str]) -> bool:
"""
check if user is known
:param username: username
:return: True in case if user is known and can be authorized and False otherwise
Args:
username(Optional[str]): username
Returns:
bool: True in case if user is known and can be authorized and False otherwise
"""
return username is not None and self.get_user(username) is not None
async def verify_access(self, username: str, required: UserAccess, context: Optional[str]) -> bool:
"""
validate if user has access to requested resource
:param username: username
:param required: required access level
:param context: URI request path
:return: True in case if user is allowed to do this request and False otherwise
Args:
username(str): username
required(UserAccess): required access level
context(Optional[str]): URI request path
Returns:
bool: True in case if user is allowed to do this request and False otherwise
"""
user = self.get_user(username)
return user is not None and user.verify_access(required)

View File

@ -32,20 +32,24 @@ class OAuth(Mapping):
"""
OAuth user authorization.
It is required to create application first and put application credentials.
:ivar client_id: application client id
:ivar client_secret: application client secret key
:ivar provider: provider class, should be one of aiohttp-client provided classes
:ivar redirect_uri: redirect URI registered in provider
:ivar scopes: list of scopes required by the application
Attributes:
client_id(str): application client id
client_secret(str): application client secret key
provider(aioauth_client.OAuth2Client): provider class, should be one of aiohttp-client provided classes
redirect_uri(str): redirect URI registered in provider
scopes(str): list of scopes required by the application
"""
def __init__(self, configuration: Configuration, database: SQLite,
provider: AuthSettings = AuthSettings.OAuth) -> None:
"""
default constructor
:param configuration: configuration instance
:param database: database instance
:param provider: authorization type definition
Args:
configuration(Configuration): configuration instance
database(SQLite): database instance
provider(AuthSettings, optional): authorization type definition (Default value = AuthSettings.OAuth)
"""
Mapping.__init__(self, configuration, database, provider)
self.client_id = configuration.get("auth", "client_id")
@ -60,7 +64,10 @@ class OAuth(Mapping):
@property
def auth_control(self) -> str:
"""
:return: login control as html code to insert
get authorization html control
Returns:
str: login control as html code to insert
"""
return """<a class="nav-link" href="/user-api/v1/login" title="login via OAuth2">login</a>"""
@ -68,8 +75,15 @@ class OAuth(Mapping):
def get_provider(name: str) -> Type[aioauth_client.OAuth2Client]:
"""
load OAuth2 provider by name
:param name: name of the provider. Must be valid class defined in aioauth-client library
:return: loaded provider type
Args:
name(str): name of the provider. Must be valid class defined in aioauth-client library
Returns:
Type[aioauth_client.OAuth2Client]: loaded provider type
Raises:
InvalidOption: in case if invalid OAuth provider name supplied
"""
provider: Type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
try:
@ -83,14 +97,18 @@ class OAuth(Mapping):
def get_client(self) -> aioauth_client.OAuth2Client:
"""
load client from parameters
:return: generated client according to current settings
Returns:
aioauth_client.OAuth2Client: generated client according to current settings
"""
return self.provider(client_id=self.client_id, client_secret=self.client_secret)
def get_oauth_url(self) -> str:
"""
get authorization URI for the specified settings
:return: authorization URI as a string
Returns:
str: authorization URI as a string
"""
client = self.get_client()
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri)
@ -99,8 +117,12 @@ class OAuth(Mapping):
async def get_oauth_username(self, code: str) -> Optional[str]:
"""
extract OAuth username from remote
:param code: authorization code provided by external service
:return: username as is in OAuth provider
Args:
code(str): authorization code provided by external service
Returns:
Optional[str]: username as is in OAuth provider
"""
try:
client = self.get_client()

View File

@ -28,7 +28,9 @@ from ahriman.core.util import check_output
class Sources:
"""
helper to download package sources (PKGBUILD etc)
:cvar logger: class logger
Attributes:
logger(logging.Logger): (class attribute) class logger
"""
logger = logging.getLogger("build_details")
@ -40,8 +42,10 @@ class Sources:
def add(sources_dir: Path, *pattern: str) -> None:
"""
track found files via git
:param sources_dir: local path to git repository
:param pattern: glob patterns
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
"""
# glob directory to find files which match the specified patterns
found_files: List[Path] = []
@ -59,8 +63,12 @@ class Sources:
def diff(sources_dir: Path) -> str:
"""
generate diff from the current version and write it to the output file
:param sources_dir: local path to git repository
:return: patch as plain string
Args:
sources_dir(Path): local path to git repository
Returns:
str: patch as plain string
"""
return Sources._check_output("git", "diff", exception=None, cwd=sources_dir, logger=Sources.logger)
@ -68,8 +76,10 @@ class Sources:
def fetch(sources_dir: Path, remote: Optional[str]) -> None:
"""
either clone repository or update it to origin/`branch`
:param sources_dir: local path to fetch
:param remote: remote target (from where to fetch)
Args:
sources_dir(Path): local path to fetch
remote(Optional[str]): remote target (from where to fetch)
"""
# local directory exists and there is .git directory
is_initialized_git = (sources_dir / ".git").is_dir()
@ -98,8 +108,12 @@ class Sources:
def has_remotes(sources_dir: Path) -> bool:
"""
check if there are remotes for the repository
:param sources_dir: local path to git repository
:return: True in case if there is any remote and false otherwise
Args:
sources_dir(Path): local path to git repository
Returns:
bool: True in case if there is any remote and false otherwise
"""
remotes = Sources._check_output("git", "remote", exception=None, cwd=sources_dir, logger=Sources.logger)
return bool(remotes)
@ -108,7 +122,9 @@ class Sources:
def init(sources_dir: Path) -> None:
"""
create empty git repository at the specified path
:param sources_dir: local path to sources
Args:
sources_dir(Path): local path to sources
"""
Sources._check_output("git", "init", "--initial-branch", Sources._branch,
exception=None, cwd=sources_dir, logger=Sources.logger)
@ -117,9 +133,11 @@ class Sources:
def load(sources_dir: Path, remote: str, patch: Optional[str]) -> None:
"""
fetch sources from remote and apply patches
:param sources_dir: local path to fetch
:param remote: remote target (from where to fetch)
:param patch: optional patch to be applied
Args:
sources_dir(Path): local path to fetch
remote(str): remote target (from where to fetch)
patch(Optional[str]): optional patch to be applied
"""
Sources.fetch(sources_dir, remote)
if patch is None:
@ -131,8 +149,10 @@ class Sources:
def patch_apply(sources_dir: Path, patch: str) -> None:
"""
apply patches if any
:param sources_dir: local path to directory with git sources
:param patch: patch to be applied
Args:
sources_dir(Path): local path to directory with git sources
patch(str): patch to be applied
"""
# create patch
Sources.logger.info("apply patch from database")
@ -143,9 +163,13 @@ class Sources:
def patch_create(sources_dir: Path, *pattern: str) -> str:
"""
create patch set for the specified local path
:param sources_dir: local path to git repository
:param pattern: glob patterns
:return: patch as plain text
Args:
sources_dir(Path): local path to git repository
*pattern(str): glob patterns
Returns:
str: patch as plain text
"""
Sources.add(sources_dir, *pattern)
diff = Sources.diff(sources_dir)

View File

@ -35,11 +35,13 @@ 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
:ivar uid: uid of the repository owner user
Attributes:
build_logger(logging.Logger): logger for build process
logger(logging.Logger): class logger
package(Package): package definitions
paths(RepositoryPaths): repository paths instance
uid(int): uid of the repository owner user
"""
_check_output = check_output
@ -47,9 +49,11 @@ class Task:
def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
"""
default constructor
:param package: package definitions
:param configuration: configuration instance
:param paths: repository paths instance
Args:
package(Package): package definitions
configuration(Configuration): configuration instance
paths(RepositoryPaths): repository paths instance
"""
self.logger = logging.getLogger("root")
self.build_logger = logging.getLogger("build_details")
@ -65,8 +69,12 @@ class Task:
def build(self, sources_path: Path) -> List[Path]:
"""
run package build
:param sources_path: path to where sources are
:return: paths of produced packages
Args:
sources_path(Path): path to where sources are
Returns:
List[Path]: paths of produced packages
"""
command = [self.build_command, "-r", str(self.paths.chroot)]
command.extend(self.archbuild_flags)
@ -91,8 +99,10 @@ class Task:
def init(self, path: Path, database: SQLite) -> None:
"""
fetch package from git
:param path: local path to fetch
:param database: database instance
Args:
path(Path): local path to fetch
database(SQLite): database instance
"""
if self.paths.cache_for(self.package.base).is_dir():
# no need to clone whole repository, just copy from cache first

View File

@ -34,12 +34,15 @@ from ahriman.models.repository_paths import RepositoryPaths
class Configuration(configparser.RawConfigParser):
"""
extension for built-in configuration parser
:ivar architecture: repository architecture
: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 SYSTEM_CONFIGURATION_PATH: default system configuration path distributed by package
Attributes:
ARCHITECTURE_SPECIFIC_SECTIONS(List[str]): (class attribute) known sections which can be architecture specific.
Required by dump and merging functions
DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
DEFAULT_LOG_LEVEL(int): (class attribute) default log level (in case of fallback)
SYSTEM_CONFIGURATION_PATH(Path): (class attribute) default system configuration path distributed by package
architecture(Optional[str]): repository architecture
path(Optional[Path]): path to root configuration file
"""
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
@ -62,21 +65,30 @@ class Configuration(configparser.RawConfigParser):
@property
def include(self) -> Path:
"""
:return: path to directory with configuration includes
get full path to include directory
Returns:
Path: path to directory with configuration includes
"""
return self.getpath("settings", "include")
@property
def logging_path(self) -> Path:
"""
:return: path to logging configuration
get full path to logging configuration
Returns:
Path: path to logging configuration
"""
return self.getpath("settings", "logging")
@property
def repository_paths(self) -> RepositoryPaths:
"""
:return: repository paths instance
construct RepositoryPaths instance based on the configuration
Returns:
RepositoryPaths: repository paths instance
"""
_, architecture = self.check_loaded()
return RepositoryPaths(self.getpath("repository", "root"), architecture)
@ -85,10 +97,14 @@ class Configuration(configparser.RawConfigParser):
def from_path(cls: Type[Configuration], path: Path, architecture: str, quiet: bool) -> Configuration:
"""
constructor with full object initialization
:param path: path to root configuration file
:param architecture: repository architecture
:param quiet: force disable any log messages
:return: configuration instance
Args:
path(Path): path to root configuration file
architecture(str): repository architecture
quiet(bool): force disable any log messages
Returns:
Configuration: configuration instance
"""
config = cls()
config.load(path)
@ -100,8 +116,15 @@ class Configuration(configparser.RawConfigParser):
def __convert_list(value: str) -> List[str]:
"""
convert string value to list of strings
:param value: string configuration value
:return: list of string from the parsed string
Args:
value(str): string configuration value
Returns:
List[str]: list of string from the parsed string
Raises:
ValueError: in case if option value contains unclosed quotes
"""
def generator() -> Generator[str, None, None]:
quote_mark = None
@ -111,7 +134,7 @@ class Configuration(configparser.RawConfigParser):
quote_mark = char
elif char == quote_mark: # quoted part ended, reset quotation
quote_mark = None
elif char == " " and quote_mark is None: # found space outside of the quotation, yield the word
elif char == " " and quote_mark is None: # found space outside the quotation, yield the word
yield word
word = ""
else: # append character to the buffer
@ -126,17 +149,25 @@ class Configuration(configparser.RawConfigParser):
def section_name(section: str, suffix: str) -> str:
"""
generate section name for sections which depends on context
:param section: section name
:param suffix: session suffix, e.g. repository architecture
:return: correct section name for repository specific section
Args:
section(str): section name
suffix(str): session suffix, e.g. repository architecture
Returns:
str: correct section name for repository specific section
"""
return f"{section}:{suffix}"
def __convert_path(self, value: str) -> Path:
"""
convert string value to path object
:param value: string configuration value
:return: path object which represents the configuration value
Args:
value(str): string configuration value
Returns:
Path: path object which represents the configuration value
"""
path = Path(value)
if self.path is None or path.is_absolute():
@ -146,7 +177,12 @@ class Configuration(configparser.RawConfigParser):
def check_loaded(self) -> Tuple[Path, str]:
"""
check if service was actually loaded
:return: configuration root path and architecture if loaded
Returns:
Tuple[Path, str]: configuration root path and architecture if loaded
Raises:
InitializeException: in case if architecture and/or path are not set
"""
if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set")
@ -155,7 +191,9 @@ class Configuration(configparser.RawConfigParser):
def dump(self) -> Dict[str, Dict[str, str]]:
"""
dump configuration to dictionary
:return: configuration dump for specific architecture
Returns:
Dict[str, Dict[str, str]]: configuration dump for specific architecture
"""
return {
section: dict(self[section])
@ -172,9 +210,16 @@ class Configuration(configparser.RawConfigParser):
"""
get type variable with fallback to old logic
Despite the fact that it has same semantics as other get* methods, but it has different argument list
:param section: section name
:param architecture: repository architecture
:return: section name and found type name
Args:
section(str): section name
architecture(str): repository architecture
Returns:
Tuple[str, str]: section name and found type name
Raises:
configparser.NoSectionError: in case if no section found
"""
group_type = self.get(section, "type", fallback=None) # new-style logic
if group_type is not None:
@ -191,7 +236,9 @@ class Configuration(configparser.RawConfigParser):
def load(self, path: Path) -> None:
"""
fully load configuration
:param path: path to root configuration file
Args:
path(Path): path to root configuration file
"""
if not path.is_file(): # fallback to the system file
path = self.SYSTEM_CONFIGURATION_PATH
@ -214,7 +261,9 @@ class Configuration(configparser.RawConfigParser):
def load_logging(self, quiet: bool) -> None:
"""
setup logging settings from configuration
:param quiet: force disable any log messages
Args:
quiet(bool): force disable any log messages
"""
try:
path = self.logging_path
@ -229,7 +278,9 @@ class Configuration(configparser.RawConfigParser):
def merge_sections(self, architecture: str) -> None:
"""
merge architecture specific sections into main configuration
:param architecture: repository architecture
Args:
architecture(str): repository architecture
"""
self.architecture = architecture
for section in self.ARCHITECTURE_SPECIFIC_SECTIONS:
@ -260,9 +311,11 @@ class Configuration(configparser.RawConfigParser):
def set_option(self, section: str, option: str, value: Optional[str]) -> None:
"""
set option. Unlike default `configparser.RawConfigParser.set` it also creates section if it does not exist
:param section: section name
:param option: option name
:param value: option value as string in parsable format
Args:
section(str): section name
option(str): option name
value(Optional[str]): option value as string in parsable format
"""
if not self.has_section(section):
self.add_section(section)

View File

@ -31,10 +31,12 @@ def migrate_data(result: MigrationResult, connection: Connection,
configuration: Configuration, paths: RepositoryPaths) -> None:
"""
perform data migration
:param result: result of the schema migration
:param connection: database connection
:param configuration: configuration instance
:param paths: repository paths instance
Args:
result(MigrationResult): result of the schema migration
connection(Connection): database connection
configuration(Configuration): configuration instance
paths(RepositoryPaths): repository paths instance
"""
# initial data migration
if result.old_version <= 0:

View File

@ -29,8 +29,10 @@ from ahriman.models.repository_paths import RepositoryPaths
def migrate_package_statuses(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for package statuses
:param connection: database connection
:param paths: repository paths instance
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
def insert_base(metadata: Package, last_status: BuildStatus) -> None:
connection.execute(

View File

@ -25,8 +25,10 @@ from ahriman.models.repository_paths import RepositoryPaths
def migrate_patches(connection: Connection, paths: RepositoryPaths) -> None:
"""
perform migration for patches
:param connection: database connection
:param paths: repository paths instance
Args:
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
root = paths.root / "patches"
if not root.is_dir():

View File

@ -25,8 +25,10 @@ from ahriman.core.configuration import Configuration
def migrate_users_data(connection: Connection, configuration: Configuration) -> None:
"""
perform migration for users
:param connection: database connection
:param configuration: configuration instance
Args:
connection(Connection): database connection
configuration(Configuration): configuration instance
"""
for section in configuration.sections():
for option, value in configuration[section].items():

View File

@ -35,14 +35,18 @@ class Migrations:
"""
simple migration wrapper for the sqlite
idea comes from https://www.ash.dev/blog/simple-migration-system-in-sqlite/
:ivar connection: database connection
:ivar logger: class logger
Attributes:
connection(Connection): database connection
logger(logging.Logger): class logger
"""
def __init__(self, connection: Connection) -> None:
"""
default constructor
:param connection: database connection
Args:
connection(Connection): database connection
"""
self.connection = connection
self.logger = logging.getLogger("database")
@ -51,8 +55,12 @@ class Migrations:
def migrate(cls: Type[Migrations], connection: Connection) -> MigrationResult:
"""
perform migrations implicitly
:param connection: database connection
:return: current schema version
Args:
connection(Connection): database connection
Returns:
MigrationResult: current schema version
"""
return cls(connection).run()
@ -61,6 +69,8 @@ class Migrations:
extract all migrations from the current package
idea comes from https://julienharbulot.com/python-dynamical-import.html
Returns:
List[Migration]: list of found migrations
"""
migrations: List[Migration] = []
package_dir = Path(__file__).resolve().parent
@ -77,7 +87,9 @@ class Migrations:
def run(self) -> MigrationResult:
"""
perform migrations
:return: current schema version
Return:
MigrationResult: current schema version
"""
migrations = self.migrations()
current_version = self.user_version()
@ -118,7 +130,9 @@ class Migrations:
def user_version(self) -> int:
"""
get schema version from sqlite database
;return: current schema version
Returns:
int: current schema version
"""
cursor = self.connection.execute("pragma user_version")
current_version: int = cursor.fetchone()["user_version"]

View File

@ -33,17 +33,25 @@ class AuthOperations(Operations):
def user_get(self, username: str) -> Optional[User]:
"""
get user by username
:param username: username
:return: user if it was found
Args:
username(str): username
Returns:
Optional[User]: user if it was found
"""
return next(iter(self.user_list(username, None)), None)
def user_list(self, username: Optional[str], access: Optional[UserAccess]) -> List[User]:
"""
get users by filter
:param username: optional filter by username
:param access: optional filter by role
:return: list of users who match criteria
Args:
username(Optional[str]): optional filter by username
access(Optional[UserAccess]): optional filter by role
Returns:
List[User]: list of users who match criteria
"""
username_filter = username.lower() if username is not None else username
access_filter = access.value if access is not None else access
@ -64,7 +72,9 @@ class AuthOperations(Operations):
def user_remove(self, username: str) -> None:
"""
remove user from storage
:param username: username
Args:
username(str): username
"""
def run(connection: Connection) -> None:
connection.execute("""delete from users where username = :username""", {"username": username.lower()})
@ -74,7 +84,9 @@ class AuthOperations(Operations):
def user_update(self, user: User) -> None:
"""
update user by username
:param user: user descriptor
Args:
user(User): user descriptor
"""
def run(connection: Connection) -> None:
connection.execute(

View File

@ -32,7 +32,9 @@ class BuildOperations(Operations):
def build_queue_clear(self, package_base: Optional[str]) -> None:
"""
remove packages from build queue
:param package_base: optional filter by package base
Args:
package_base(Optional[str]): optional filter by package base
"""
def run(connection: Connection) -> None:
connection.execute(
@ -47,7 +49,9 @@ class BuildOperations(Operations):
def build_queue_get(self) -> List[Package]:
"""
retrieve packages from build queue
:return: list of packages to be built
Return:
List[Package]: list of packages to be built
"""
def run(connection: Connection) -> List[Package]:
return [
@ -60,7 +64,9 @@ class BuildOperations(Operations):
def build_queue_insert(self, package: Package) -> None:
"""
insert packages to build queue
:param package: package to be inserted
Args:
package(Package): package to be inserted
"""
def run(connection: Connection) -> None:
connection.execute(

View File

@ -31,14 +31,18 @@ T = TypeVar("T")
class Operations:
"""
base operation class
:ivar logger: class logger
:ivar path: path to the database file
Attributes:
logger(logging.Logger): class logger
path(Path): path to the database file
"""
def __init__(self, path: Path) -> None:
"""
default constructor
:param path: path to the database file
Args:
path(Path): path to the database file
"""
self.path = path
self.logger = logging.getLogger("database")
@ -47,9 +51,13 @@ class Operations:
def factory(cursor: Cursor, row: Tuple[Any, ...]) -> Dict[str, Any]:
"""
dictionary factory based on official documentation
:param cursor: cursor descriptor
:param row: fetched row
:return: row converted to dictionary
Args:
cursor(Cursor): cursor descriptor
row(Tuple[Any, ...]): fetched row
Returns:
Dict[str, Any]: row converted to dictionary
"""
result = {}
for index, column in enumerate(cursor.description):
@ -59,9 +67,13 @@ class Operations:
def with_connection(self, query: Callable[[Connection], T], commit: bool = False) -> T:
"""
perform operation in connection
:param query: function to be called with connection
:param commit: if True commit() will be called on success
:return: result of the `query` call
Args:
query(Callable[[Connection], T]): function to be called with connection
commit(bool, optional): if True commit() will be called on success (Default value = False)
Returns:
T: result of the `query` call
"""
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
connection.row_factory = self.factory

View File

@ -35,8 +35,10 @@ class PackageOperations(Operations):
def _package_remove_package_base(connection: Connection, package_base: str) -> None:
"""
remove package base information
:param connection: database connection
:param package_base: package base name
Args:
connection(Connection): database connection
package_base(str): package base name
"""
connection.execute("""delete from package_statuses where package_base = :package_base""",
{"package_base": package_base})
@ -47,9 +49,11 @@ class PackageOperations(Operations):
def _package_remove_packages(connection: Connection, package_base: str, current_packages: Iterable[str]) -> None:
"""
remove packages belong to the package base
:param connection: database connection
:param package_base: package base name
:param current_packages: current packages list which has to be left in database
Args:
connection(Connection): database connection
package_base(str): package base name
current_packages(Iterable[str]): current packages list which has to be left in database
"""
packages = [
package
@ -63,8 +67,10 @@ class PackageOperations(Operations):
def _package_update_insert_base(connection: Connection, package: Package) -> None:
"""
insert base package into table
:param connection: database connection
:param package: package properties
Args:
connection(Connection): database connection
package(Package): package properties
"""
connection.execute(
"""
@ -81,8 +87,10 @@ class PackageOperations(Operations):
def _package_update_insert_packages(connection: Connection, package: Package) -> None:
"""
insert packages into table
:param connection: database connection
:param package: package properties
Args:
connection(Connection): database connection
package(Package): package properties
"""
package_list = []
for name, description in package.packages.items():
@ -108,9 +116,11 @@ class PackageOperations(Operations):
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus) -> None:
"""
insert base package status into table
:param connection: database connection
:param package_base: package base name
:param status: new build status
Args:
connection(Connection): database connection
package_base(str): package base name
status(BuildStatus): new build status
"""
connection.execute(
"""
@ -126,8 +136,12 @@ class PackageOperations(Operations):
def _packages_get_select_package_bases(connection: Connection) -> Dict[str, Package]:
"""
select package bases from the table
:param connection: database connection
:return: map of the package base to its descriptor (without packages themselves)
Args:
connection(Connection): database connection
Returns:
Dict[str, Package]: map of the package base to its descriptor (without packages themselves)
"""
return {
row["package_base"]: Package(row["package_base"], row["version"], row["aur_url"], {})
@ -138,9 +152,13 @@ class PackageOperations(Operations):
def _packages_get_select_packages(connection: Connection, packages: Dict[str, Package]) -> Dict[str, Package]:
"""
select packages from the table
:param connection: database connection
:param packages: packages descriptor map
:return: map of the package base to its descriptor including individual packages
Args:
connection(Connection): database connection
packages(Dict[str, Package]): packages descriptor map
Returns:
Dict[str, Package]: map of the package base to its descriptor including individual packages
"""
for row in connection.execute("""select * from packages"""):
if row["package_base"] not in packages:
@ -152,8 +170,12 @@ class PackageOperations(Operations):
def _packages_get_select_statuses(connection: Connection) -> Dict[str, BuildStatus]:
"""
select package build statuses from the table
:param connection: database connection
:return: map of the package base to its status
Args:
connection(Connection): database connection
Returns:
Dict[str, BuildStatus]: map of the package base to its status
"""
return {
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
@ -163,7 +185,9 @@ class PackageOperations(Operations):
def package_remove(self, package_base: str) -> None:
"""
remove package from database
:param package_base: package base name
Args:
package_base(str): package base name
"""
def run(connection: Connection) -> None:
self._package_remove_packages(connection, package_base, [])
@ -174,8 +198,10 @@ class PackageOperations(Operations):
def package_update(self, package: Package, status: BuildStatus) -> None:
"""
update package status
:param package: package properties
:param status: new build status
Args:
package(Package): package properties
status(BuildStatus): new build status
"""
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package)
@ -188,7 +214,9 @@ class PackageOperations(Operations):
def packages_get(self) -> List[Tuple[Package, BuildStatus]]:
"""
get package list and their build statuses from database
:return: list of package properties and their statuses
Return:
List[Tuple[Package, BuildStatus]]: list of package properties and their statuses
"""
def run(connection: Connection) -> Generator[Tuple[Package, BuildStatus], None, None]:
packages = self._packages_get_select_package_bases(connection)

View File

@ -31,16 +31,22 @@ class PatchOperations(Operations):
def patches_get(self, package_base: str) -> Optional[str]:
"""
retrieve patches for the package
:param package_base: package base to search for patches
:return: plain text patch for the package
Args:
package_base(str): package base to search for patches
Returns:
Optional[str]: plain text patch for the package
"""
return self.patches_list(package_base).get(package_base)
def patches_insert(self, package_base: str, patch: str) -> None:
"""
insert or update patch in database
:param package_base: package base to insert
:param patch: patch content
Args:
package_base(str): package base to insert
patch(str): patch content
"""
def run(connection: Connection) -> None:
connection.execute(
@ -59,8 +65,12 @@ class PatchOperations(Operations):
def patches_list(self, package_base: Optional[str]) -> Dict[str, str]:
"""
extract all patches
:param package_base: optional filter by package base
:return: map of package base to patch content
Args:
package_base(Optional[str]): optional filter by package base
Returns:
Dict[str, str]: map of package base to patch content
"""
def run(connection: Connection) -> Dict[str, str]:
return {
@ -75,7 +85,9 @@ class PatchOperations(Operations):
def patches_remove(self, package_base: str) -> None:
"""
remove patch set
:param package_base: package base to clear patches
Args:
package_base(str): package base to clear patches
"""
def run(connection: Connection) -> None:
connection.execute(

View File

@ -44,8 +44,12 @@ class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations
def load(cls: Type[SQLite], configuration: Configuration) -> SQLite:
"""
construct instance from configuration
:param configuration: configuration instance
:return: fully initialized instance of the database
Args:
configuration(Configuration): configuration instance
Returns:
SQLite: fully initialized instance of the database
"""
path = cls.database_path(configuration)
database = cls(path)
@ -56,15 +60,21 @@ class SQLite(AuthOperations, BuildOperations, PackageOperations, PatchOperations
def database_path(configuration: Configuration) -> Path:
"""
read database from configuration
:param configuration: configuration instance
:return: database path according to the configuration
Args:
configuration(Configuration): configuration instance
Returns:
Path: database path according to the configuration
"""
return configuration.getpath("settings", "database")
def init(self, configuration: Configuration) -> None:
"""
perform database migrations
:param configuration: configuration instance
Args:
configuration(Configuration): configuration instance
"""
# custom types support
sqlite3.register_adapter(dict, json.dumps)

View File

@ -29,7 +29,9 @@ class BuildFailed(RuntimeError):
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base raised exception
Args:
package_base(str): package base raised exception
"""
RuntimeError.__init__(self, f"Package {package_base} build failed, check logs for details")
@ -61,7 +63,9 @@ class InitializeException(RuntimeError):
def __init__(self, details: str) -> None:
"""
default constructor
:param details: details of the exception
Args:
details(str): details of the exception
"""
RuntimeError.__init__(self, f"Could not load service: {details}")
@ -74,7 +78,9 @@ class InvalidOption(ValueError):
def __init__(self, value: Any) -> None:
"""
default constructor
:param value: option value
Args:
value(Any): option value
"""
ValueError.__init__(self, f"Invalid or unknown option value `{value}`")
@ -87,8 +93,10 @@ class InvalidPath(ValueError):
def __init__(self, path: Path, root: Path) -> None:
"""
default constructor
:param path: path which raised an exception
:param root: repository root (i.e. ahriman home)
Args:
path(Path): path which raised an exception
root(Path): repository root (i.e. ahriman home)
"""
ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`")
@ -101,7 +109,9 @@ class InvalidPackageInfo(RuntimeError):
def __init__(self, details: Any) -> None:
"""
default constructor
:param details: error details
Args:
details(Any): error details
"""
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`")
@ -114,7 +124,9 @@ class MigrationError(RuntimeError):
def __init__(self, details: str) -> None:
"""
default constructor
:param details: error details
Args:
details(str): error details
"""
RuntimeError.__init__(self, details)
@ -127,7 +139,9 @@ class MissingArchitecture(ValueError):
def __init__(self, command: str) -> None:
"""
default constructor
:param command: command name which throws exception
Args:
command(str): command name which throws exception
"""
ValueError.__init__(self, f"Architecture required for subcommand {command}, but missing")
@ -140,7 +154,9 @@ class MultipleArchitectures(ValueError):
def __init__(self, command: str) -> None:
"""
default constructor
:param command: command name which throws exception
Args:
command(str): command name which throws exception
"""
ValueError.__init__(self, f"Multiple architectures are not supported by subcommand {command}")
@ -165,7 +181,9 @@ class SuccessFailed(ValueError):
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base name
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
@ -190,7 +208,9 @@ class UnknownPackage(ValueError):
def __init__(self, package_base: str) -> None:
"""
default constructor
:param package_base: package base name
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} is unknown")
@ -203,8 +223,10 @@ class UnsafeRun(RuntimeError):
def __init__(self, current_uid: int, root_uid: int) -> None:
"""
default constructor
:param current_uid: current user ID
:param root_uid: ID of the owner of root directory
Args:
current_uid(int): current user ID
root_uid(int): ID of the owner of root directory
"""
RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. "
f"Note that for the most actions it is unsafe to run application as different user."

View File

@ -28,13 +28,17 @@ from ahriman.models.property import Property
class AurPrinter(StringPrinter):
"""
print content of the AUR package
:ivar package: AUR package description
Attributes:
package(AURPackage): AUR package description
"""
def __init__(self, package: AURPackage) -> None:
"""
default constructor
:param package: AUR package description
Args:
package(AURPackage): AUR package description
"""
StringPrinter.__init__(self, f"{package.name} {package.version} ({package.num_votes})")
self.package = package
@ -42,7 +46,9 @@ class AurPrinter(StringPrinter):
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
Returns:
List[Property]: list of content properties
"""
return [
Property("Package base", self.package.package_base),

View File

@ -29,9 +29,11 @@ class BuildPrinter(StringPrinter):
def __init__(self, package: Package, is_success: bool, use_utf: bool) -> None:
"""
default constructor
:param package: built package
:param is_success: True in case if build has success status and False otherwise
:param use_utf: use utf instead of normal symbols
Args:
package(Package): built package
is_success(bool): True in case if build has success status and False otherwise
use_utf(bool): use utf instead of normal symbols
"""
StringPrinter.__init__(self, f"{self.sign(is_success, use_utf)} {package.base}")
@ -39,9 +41,13 @@ class BuildPrinter(StringPrinter):
def sign(is_success: bool, use_utf: bool) -> str:
"""
generate sign according to settings
:param use_utf: use utf instead of normal symbols
:param is_success: True in case if build has success status and False otherwise
:return: sign symbol according to current settings
Args:
is_success(bool): True in case if build has success status and False otherwise
use_utf(bool): use utf instead of normal symbols
Returns:
str: sign symbol according to current settings
"""
if is_success:
return "[✔]" if use_utf else "[x]"

View File

@ -26,14 +26,18 @@ from ahriman.models.property import Property
class ConfigurationPrinter(StringPrinter):
"""
print content of the configuration section
:ivar values: configuration values dictionary
Attributes:
values(Dict[str, str]): configuration values dictionary
"""
def __init__(self, section: str, values: Dict[str, str]) -> None:
"""
default constructor
:param section: section name
:param values: configuration values dictionary
Args:
section(str): section name
values(Dict[str, str]): configuration values dictionary
"""
StringPrinter.__init__(self, f"[{section}]")
self.values = values
@ -41,7 +45,9 @@ class ConfigurationPrinter(StringPrinter):
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
Returns:
List[Property]: list of content properties
"""
return [
Property(key, value, is_required=True)

View File

@ -28,15 +28,19 @@ from ahriman.models.property import Property
class PackagePrinter(StringPrinter):
"""
print content of the internal package object
:ivar package: package description
:ivar status: build status
Attributes:
package(Package): package description
status(BuildStatus): build status
"""
def __init__(self, package: Package, status: BuildStatus) -> None:
"""
default constructor
:param package: package description
:param status: build status
Args:
package(Package): package description
status(BuildStatus): build status
"""
StringPrinter.__init__(self, package.pretty_print())
self.package = package
@ -45,7 +49,9 @@ class PackagePrinter(StringPrinter):
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
Returns:
List[Property]: list of content properties
"""
return [
Property("Version", self.package.version, is_required=True),

View File

@ -30,9 +30,12 @@ class Printer:
def print(self, verbose: bool, log_fn: Callable[[str], None] = print, separator: str = ": ") -> None:
"""
print content
:param verbose: print all fields
:param log_fn: logger function to log data
:param separator: separator for property name and property value
Args:
verbose(bool): print all fields
log_fn(Callable[[str]): logger function to log data
None]: (Default value = print)
separator(str, optional): separator for property name and property value (Default value = ": ")
"""
if (title := self.title()) is not None:
log_fn(title)
@ -44,12 +47,16 @@ class Printer:
def properties(self) -> List[Property]: # pylint: disable=no-self-use
"""
convert content into printable data
:return: list of content properties
Returns:
List[Property]: list of content properties
"""
return []
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
Returns:
Optional[str]: content title if it can be generated and None otherwise
"""

View File

@ -29,6 +29,8 @@ class StatusPrinter(StringPrinter):
def __init__(self, status: BuildStatus) -> None:
"""
default constructor
:param status: build status
Args:
status(BuildStatus): build status
"""
StringPrinter.__init__(self, status.pretty_print())

View File

@ -30,13 +30,17 @@ class StringPrinter(Printer):
def __init__(self, content: str) -> None:
"""
default constructor
:param content: any content string
Args:
content(str): any content string
"""
self.content = content
def title(self) -> Optional[str]:
"""
generate entry title from content
:return: content title if it can be generated and None otherwise
Returns:
Optional[str]: content title if it can be generated and None otherwise
"""
return self.content

View File

@ -27,15 +27,19 @@ from ahriman.models.property import Property
class UpdatePrinter(StringPrinter):
"""
print content of the package update
:ivar package: remote (new) package object
:ivar local_version: local version of the package if any
Attributes:
package(Package): remote (new) package object
local_version(Optional[str]): local version of the package if any
"""
def __init__(self, remote: Package, local_version: Optional[str]) -> None:
"""
default constructor
:param remote: remote (new) package object
:param local_version: local version of the package if any
Args:
remote(Package): remote (new) package object
local_version(Optional[str]): local version of the package if any
"""
StringPrinter.__init__(self, remote.base)
self.package = remote
@ -44,6 +48,8 @@ class UpdatePrinter(StringPrinter):
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
Returns:
List[Property]: list of content properties
"""
return [Property(self.local_version, self.package.version, is_required=True)]

View File

@ -27,13 +27,17 @@ from ahriman.models.user import User
class UserPrinter(StringPrinter):
"""
print properties of user
:ivar user: stored user
Attributes:
user(User): stored user
"""
def __init__(self, user: User) -> None:
"""
default constructor
:param user: user to print
Args:
user(User): user to print
"""
StringPrinter.__init__(self, user.username)
self.user = user
@ -41,6 +45,8 @@ class UserPrinter(StringPrinter):
def properties(self) -> List[Property]:
"""
convert content into printable data
:return: list of content properties
Returns:
List[Property]: list of content properties
"""
return [Property("role", self.user.access.value, is_required=True)]

View File

@ -29,15 +29,19 @@ from ahriman.models.result import Result
class Console(Report):
"""
html report generator
:ivar use_utf: print utf8 symbols instead of ASCII
Attributes:
use_utf(bool): print utf8 symbols instead of ASCII
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Report.__init__(self, architecture, configuration)
self.use_utf = configuration.getboolean(section, "use_utf", fallback=True)
@ -45,8 +49,10 @@ class Console(Report):
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
"""
for package in result.success:
BuildPrinter(package, is_success=True, use_utf=self.use_utf).print(verbose=True)

View File

@ -36,24 +36,28 @@ from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
class Email(Report, JinjaTemplate):
"""
email report generator
:ivar full_template_path: path to template for full package list
:ivar host: SMTP host to connect
:ivar no_empty_report: skip empty report generation
:ivar password: password to authenticate via SMTP
:ivar port: SMTP port to connect
:ivar receivers: list of receivers emails
:ivar sender: sender email address
:ivar ssl: SSL mode for SMTP connection
:ivar template_path: path to template for built packages
:ivar user: username to authenticate via SMTP
Attributes:
full_template_path(Path): path to template for full package list
host(str): SMTP host to connect
no_empty_report(bool): skip empty report generation
password(Optional[str]): password to authenticate via SMTP
port(int): SMTP port to connect
receivers(List[str]): list of receivers emails
sender(str): sender email address
ssl(SmtpSSLSettings): SSL mode for SMTP connection
template_path(Path): path to template for built packages
user(Optional[str]): username to authenticate via SMTP
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, section, configuration)
@ -74,8 +78,10 @@ class Email(Report, JinjaTemplate):
def _send(self, text: str, attachment: Dict[str, str]) -> None:
"""
send email callback
:param text: email body text
:param attachment: map of attachment filename to attachment content
Args:
text(str): email body text
attachment(Dict[str, str]): map of attachment filename to attachment content
"""
message = MIMEMultipart()
message["From"] = self.sender
@ -102,8 +108,10 @@ class Email(Report, JinjaTemplate):
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
"""
if self.no_empty_report and not result.success:
return

View File

@ -29,16 +29,20 @@ from ahriman.models.result import Result
class HTML(Report, JinjaTemplate):
"""
html report generator
:ivar report_path: output path to html report
:ivar template_path: path to template for full package list
Attributes:
report_path(Path): output path to html report
template_path(Path): path to template for full package list
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, section, configuration)
@ -49,8 +53,10 @@ class HTML(Report, JinjaTemplate):
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
"""
html = self.make_html(Result(success=packages), self.template_path)
self.report_path.write_text(html)

View File

@ -35,38 +35,41 @@ class JinjaTemplate:
It uses jinja2 templates for report generation, the following variables are allowed:
homepage - link to homepage, string, optional
link_path - prefix fo packages to download, string, required
has_package_signed - True in case if package sign enabled, False otherwise, required
has_repo_signed - True in case if repository database sign enabled, False otherwise, required
packages - sorted list of packages properties, required
* architecture, string
* archive_size, pretty printed size, string
* build_date, pretty printed datetime, string
* depends, sorted list of strings
* description, string
* filename, string,
* groups, sorted list of strings
* installed_size, pretty printed datetime, string
* licenses, sorted list of strings
* name, string
* url, string
* version, string
pgp_key - default PGP key ID, string, optional
repository - repository name, string, required
* homepage - link to homepage, string, optional
* link_path - prefix fo packages to download, string, required
* has_package_signed - True in case if package sign enabled, False otherwise, required
* has_repo_signed - True in case if repository database sign enabled, False otherwise, required
* packages - sorted list of packages properties, required
* architecture, string
* archive_size, pretty printed size, string
* build_date, pretty printed datetime, string
* depends, sorted list of strings
* description, string
* filename, string,
* groups, sorted list of strings
* installed_size, pretty printed datetime, string
* licenses, sorted list of strings
* name, string
* url, string
* version, string
* pgp_key - default PGP key ID, string, optional
* repository - repository name, string, required
:ivar homepage: homepage link if any (for footer)
:ivar link_path: prefix fo packages to download
:ivar name: repository name
:ivar default_pgp_key: default PGP key
:ivar sign_targets: targets to sign enabled in configuration
Attributes:
homepage(Optional[str]): homepage link if any (for footer)
link_path(str): prefix fo packages to download
name(str): repository name
default_pgp_key(Optional[str]): default PGP key
sign_targets(Set[SignSettings]): targets to sign enabled in configuration
"""
def __init__(self, section: str, configuration: Configuration) -> None:
"""
default constructor
:param section: settings section name
:param configuration: configuration instance
Args:
section(str): settings section name
configuration(Configuration): configuration instance
"""
self.link_path = configuration.get(section, "link_path")
@ -79,8 +82,10 @@ class JinjaTemplate:
def make_html(self, result: Result, template_path: Path) -> str:
"""
generate report for the specified packages
:param result: build result
:param template_path: path to jinja template
Args:
result(Result): build result
template_path(Path): path to jinja template
"""
# idea comes from https://stackoverflow.com/a/38642558
loader = jinja2.FileSystemLoader(searchpath=template_path.parent)

View File

@ -33,16 +33,20 @@ from ahriman.models.result import Result
class Report:
"""
base report generator
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar logger: class logger
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
logger(logging.Logger): class logger
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
@ -52,10 +56,14 @@ class Report:
def load(cls: Type[Report], architecture: str, configuration: Configuration, target: str) -> Report:
"""
load client from settings
:param architecture: repository architecture
:param configuration: configuration instance
:param target: target to generate report aka section name (e.g. html)
:return: client according to current settings
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
target(str): target to generate report aka section name (e.g. html)
Returns:
Report: client according to current settings
"""
section, provider_name = configuration.gettype(target, architecture)
provider = ReportSettings.from_option(provider_name)
@ -76,15 +84,22 @@ class Report:
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
"""
def run(self, packages: Iterable[Package], result: Result) -> None:
"""
run report generation
:param packages: list of packages to generate report
:param result: build result
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
Raises:
ReportFailed: in case of any report unmatched exception
"""
try:
self.generate(packages, result)

View File

@ -33,12 +33,14 @@ from ahriman.models.result import Result
class Telegram(Report, JinjaTemplate):
"""
telegram report generator
:cvar TELEGRAM_API_URL: telegram api base url
:cvar TELEGRAM_MAX_CONTENT_LENGTH: max content length of the message
:ivar api_key: bot api key
:ivar chat_id: chat id to post message, either string with @ or integer
:ivar template_path: path to template for built packages
:ivar template_type: template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
Attributes:
TELEGRAM_API_URL(str): (class attribute) telegram api base url
TELEGRAM_MAX_CONTENT_LENGTH(int): (class attribute) max content length of the message
api_key(str): bot api key
chat_id(str): chat id to post message, either string with @ or integer
template_path(Path): path to template for built packages
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
"""
TELEGRAM_API_URL = "https://api.telegram.org"
@ -47,9 +49,11 @@ class Telegram(Report, JinjaTemplate):
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, section, configuration)
@ -62,7 +66,9 @@ class Telegram(Report, JinjaTemplate):
def _send(self, text: str) -> None:
"""
send message to telegram channel
:param text: message body text
Args:
text(str): message body text
"""
try:
response = requests.post(
@ -79,8 +85,10 @@ class Telegram(Report, JinjaTemplate):
def generate(self, packages: Iterable[Package], result: Result) -> None:
"""
generate report for the specified packages
:param packages: list of packages to generate report
:param result: build result
Args:
packages(Iterable[Package]): list of packages to generate report
result(Result): build result
"""
if not result.success:
return

View File

@ -33,7 +33,12 @@ class Cleaner(Properties):
def packages_built(self) -> List[Path]:
"""
get list of files in built packages directory
:return: list of filenames from the directory
Returns:
List[Path]: list of filenames from the directory
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError

View File

@ -39,23 +39,39 @@ class Executor(Cleaner):
def load_archives(self, packages: Iterable[Path]) -> List[Package]:
"""
load packages from list of archives
:param packages: paths to package archives
:return: list of read packages
Args:
packages(Iterable[Path]): paths to package archives
Returns:
List[Package]: list of read packages
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def packages(self) -> List[Package]:
"""
generate list of repository packages
:return: list of packages properties
Returns:
List[Package]: list of packages properties
Raises:
NotImplementedError: not implemented method
"""
raise NotImplementedError
def process_build(self, updates: Iterable[Package]) -> Result:
"""
build packages
:param updates: list of packages properties to build
:return: `packages_built`
Args:
updates(Iterable[Package]): list of packages properties to build
Returns:
Result: build result
"""
def build_single(package: Package, local_path: Path) -> None:
self.reporter.set_building(package.base)
@ -82,8 +98,12 @@ class Executor(Cleaner):
def process_remove(self, packages: Iterable[str]) -> Path:
"""
remove packages from list
:param packages: list of package names or bases to remove
:return: path to repository database
Args:
packages(Iterable[str]): list of package names or bases to remove
Returns:
Path: path to repository database
"""
def remove_base(package_base: str) -> None:
try:
@ -126,8 +146,11 @@ class Executor(Cleaner):
def process_report(self, targets: Optional[Iterable[str]], result: Result) -> None:
"""
generate reports
:param targets: list of targets to generate reports. Configuration option will be used if it is not set
:param result: build result
Args:
targets(Optional[Iterable[str]]): list of targets to generate reports. Configuration option will be used
if it is not set
result(Result): build result
"""
if targets is None:
targets = self.configuration.getlist("report", "target")
@ -138,8 +161,11 @@ class Executor(Cleaner):
def process_sync(self, targets: Optional[Iterable[str]], built_packages: Iterable[Package]) -> None:
"""
process synchronization to remote servers
:param targets: list of targets to sync. Configuration option will be used if it is not set
:param built_packages: list of packages which has just been built
Args:
targets(Optional[Iterable[str]]): list of targets to sync. Configuration option will be used
if it is not set
built_packages(Iterable[Package]): list of packages which has just been built
"""
if targets is None:
targets = self.configuration.getlist("upload", "target")
@ -150,8 +176,12 @@ class Executor(Cleaner):
def process_update(self, packages: Iterable[Path]) -> Result:
"""
sign packages, add them to repository and update repository database
:param packages: list of filenames to run
:return: path to repository database
Args:
packages(Iterable[Path]): list of filenames to run
Returns:
Result: path to repository database
"""
def update_single(name: Optional[str], base: str) -> None:
if name is None:

View File

@ -32,29 +32,33 @@ from ahriman.core.util import check_user
class Properties:
"""
repository internal objects holder
:ivar architecture: repository architecture
:ivar aur_url: base AUR url
:ivar configuration: configuration instance
:ivar database: database instance
:ivar ignore_list: package bases which will be ignored during auto updates
:ivar logger: class logger
:ivar name: repository name
:ivar pacman: alpm wrapper instance
:ivar paths: repository paths instance
:ivar repo: repo commands wrapper instance
:ivar reporter: build status reporter instance
:ivar sign: GPG wrapper instance
Attributes:
architecture(str): repository architecture
aur_url(str): base AUR url
configuration(Configuration): configuration instance
database(SQLite): database instance
ignore_list(List[str]): package bases which will be ignored during auto updates
logger(logging.Logger): class logger
name(str): repository name
pacman(Pacman): alpm wrapper instance
paths(RepositoryPaths): repository paths instance
repo(Repo): repo commands wrapper instance
reporter(Client): build status reporter instance
sign(GPG): GPG wrapper instance
"""
def __init__(self, architecture: str, configuration: Configuration, database: SQLite,
no_report: bool, unsafe: bool) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param database: database instance
:param no_report: force disable reporting
:param unsafe: if set no user check will be performed before path creation
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
database(SQLite): database instance
no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation
"""
self.logger = logging.getLogger("root")
self.architecture = architecture

View File

@ -35,8 +35,12 @@ class Repository(Executor, UpdateHandler):
def load_archives(self, packages: Iterable[Path]) -> List[Package]:
"""
load packages from list of archives
:param packages: paths to package archives
:return: list of read packages
Args:
packages(Iterable[Path]): paths to package archives
Returns:
List[Package]: list of read packages
"""
result: Dict[str, Package] = {}
# we are iterating over bases, not single packages
@ -58,22 +62,30 @@ class Repository(Executor, UpdateHandler):
def packages(self) -> List[Package]:
"""
generate list of repository packages
:return: list of packages properties
Returns:
List[Package]: list of packages properties
"""
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
def packages_built(self) -> List[Path]:
"""
get list of files in built packages directory
:return: list of filenames from the directory
Returns:
List[Path]: list of filenames from the directory
"""
return list(filter(package_like, self.paths.packages.iterdir()))
def packages_depends_on(self, depends_on: Optional[Iterable[str]]) -> List[Package]:
"""
extract list of packages which depends on specified package
:param: depends_on: dependencies of the packages
:return: list of repository packages which depend on specified packages
Args:
depends_on(Optional[Iterable[str]]): dependencies of the packages
Returns:
List[Package]: list of repository packages which depend on specified packages
"""
packages = self.packages()
if depends_on is None:

View File

@ -33,16 +33,25 @@ class UpdateHandler(Cleaner):
def packages(self) -> List[Package]:
"""
generate list of repository packages
:return: list of packages properties
Returns:
List[Package]: list of packages properties
Raises:
NotImplementedError: not implemented method
"""
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
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
no_vcs(bool): do not check VCS packages
Returns:
List[Package]: list of packages which are out-of-dated
"""
result: List[Package] = []
@ -70,7 +79,9 @@ class UpdateHandler(Cleaner):
def updates_local(self) -> List[Package]:
"""
check local packages for updates
:return: list of local packages which are out-of-dated
Returns:
List[Package]: list of local packages which are out-of-dated
"""
result: List[Package] = []
packages = {local.base: local for local in self.packages()}
@ -97,7 +108,9 @@ class UpdateHandler(Cleaner):
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
Returns:
List[Package]: list of packages which are out-of-dated
"""
result: List[Package] = []
known_bases = {package.base for package in self.packages()}

View File

@ -32,11 +32,13 @@ from ahriman.models.sign_settings import SignSettings
class GPG:
"""
gnupg wrapper
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar default_key: default PGP key ID to use
:ivar logger: class logger
:ivar targets: list of targets to sign (repository, package etc)
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
default_key(Optional[str]): default PGP key ID to use
logger(logging.Logger): class logger
targets(Set[SignSettings]): list of targets to sign (repository, package etc)
"""
_check_output = check_output
@ -44,8 +46,10 @@ class GPG:
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("build_details")
self.architecture = architecture
@ -55,7 +59,10 @@ class GPG:
@property
def repository_sign_args(self) -> List[str]:
"""
:return: command line arguments for repo-add command to sign database
get command line arguments based on settings
Returns:
List[str]: command line arguments for repo-add command to sign database
"""
if SignSettings.Repository not in self.targets:
return []
@ -68,9 +75,13 @@ class GPG:
def sign_command(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
Args:
path(Path): path to file to sign
key(str): PGP key ID
Returns:
List[str]: gpg command with all required arguments
"""
return ["gpg", "-u", key, "-b", str(path)]
@ -78,22 +89,32 @@ class GPG:
def sign_options(configuration: Configuration) -> Tuple[Set[SignSettings], Optional[str]]:
"""
extract default sign options from configuration
:param configuration: configuration instance
:return: tuple of sign targets and default PGP key
Args:
configuration(Configuration): configuration instance
Returns:
Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key
"""
targets = {
SignSettings.from_option(option)
for option in configuration.getlist("sign", "target")
}
targets: Set[SignSettings] = set()
for option in configuration.getlist("sign", "target"):
target = SignSettings.from_option(option)
if target == SignSettings.Disabled:
continue
targets.add(target)
default_key = configuration.get("sign", "key") if targets else None
return targets, default_key
def key_download(self, server: str, key: str) -> str:
"""
download key from public PGP server
:param server: public PGP server which will be used to download the key
:param key: key ID to download
:return: key as plain text
Args:
server(str): public PGP server which will be used to download the key
key(str): key ID to download
Returns:
str: key as plain text
"""
key = key if key.startswith("0x") else f"0x{key}"
try:
@ -111,8 +132,10 @@ class GPG:
def key_import(self, server: str, key: str) -> None:
"""
import key to current user and sign it locally
:param server: public PGP server which will be used to download the key
:param key: key ID to import
Args:
server(str): public PGP server which will be used to download the key
key(str): key ID to import
"""
key_body = self.key_download(server, key)
GPG._check_output("gpg", "--import", input_data=key_body, exception=None, logger=self.logger)
@ -120,9 +143,13 @@ class GPG:
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
Args:
path(Path): path to file to sign
key(str): PGP key ID
Returns:
List[Path]: list of generated files including original file
"""
GPG._check_output(
*GPG.sign_command(path, key),
@ -133,9 +160,13 @@ class GPG:
def process_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
Args:
path(Path): path to file to sign
base(str): package base required to check for key overrides
Returns:
List[Path]: list of generated files including original file
"""
if SignSettings.Packages not in self.targets:
return [path]
@ -149,8 +180,12 @@ class GPG:
"""
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
Args:
path(Path): path to repository database
Returns:
List[Path]: list of generated files including original file
"""
if SignSettings.Repository not in self.targets:
return [path]

View File

@ -35,19 +35,23 @@ class Spawn(Thread):
"""
helper to spawn external ahriman process
MUST NOT be used directly, the only one usage allowed is to spawn process from web services
:ivar active: map of active child processes required to avoid zombies
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar logger: spawner logger
:ivar queue: multiprocessing queue to read updates from processes
Attributes:
active(Dict[str, Process]): map of active child processes required to avoid zombies
architecture(str): repository architecture
configuration(Configuration): configuration instance
logger(logging.Logger): spawner logger
queue(Queue[Tuple[str, bool]]): multiprocessing queue to read updates from processes
"""
def __init__(self, args_parser: argparse.ArgumentParser, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param args_parser: command line parser for the application
:param architecture: repository architecture
:param configuration: configuration instance
Args:
args_parser(argparse.ArgumentParser): command line parser for the application
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
Thread.__init__(self, name="spawn")
self.architecture = architecture
@ -65,11 +69,13 @@ class Spawn(Thread):
process_id: str, queue: Queue[Tuple[str, bool]]) -> None: # pylint: disable=unsubscriptable-object
"""
helper to run external process
:param callback: application run function (i.e. Handler.run method)
:param args: command line arguments
:param architecture: repository architecture
:param process_id: process unique identifier
:param queue: output queue
Args:
callback(Callable[[argparse.Namespace, str], bool]): application run function (i.e. Handler.run method)
args(argparse.Namespace): command line arguments
architecture(str): repository architecture
process_id(str): process unique identifier
queue(Queue[Tuple[str, bool]]): output queue
"""
result = callback(args, architecture)
queue.put((process_id, result))
@ -77,8 +83,10 @@ class Spawn(Thread):
def packages_add(self, packages: Iterable[str], now: bool) -> None:
"""
add packages
:param packages: packages list to add
:param now: build packages now
Args:
packages(Iterable[str]): packages list to add
now(bool): build packages now
"""
kwargs = {"source": PackageSource.AUR.value} # avoid abusing by building non-aur packages
if now:
@ -88,16 +96,20 @@ class Spawn(Thread):
def packages_remove(self, packages: Iterable[str]) -> None:
"""
remove packages
:param packages: packages list to remove
Args:
packages(Iterable[str]): packages list to remove
"""
self.spawn_process("remove", *packages)
def spawn_process(self, command: str, *args: str, **kwargs: str) -> None:
"""
spawn external ahriman process with supplied arguments
:param command: subcommand to run
:param args: positional command arguments
:param kwargs: named command arguments
Args:
command(str): subcommand to run
*args(str): positional command arguments
**kwargs(str): named command arguments
"""
# default arguments
arguments = ["--architecture", self.architecture]

View File

@ -36,8 +36,12 @@ class Client:
def load(cls: Type[Client], configuration: Configuration) -> Client:
"""
load client from settings
:param configuration: configuration instance
:return: client according to current settings
Args:
configuration(Configuration): configuration instance
Returns:
Client: client according to current settings
"""
address = configuration.get("web", "address", fallback=None)
host = configuration.get("web", "host", fallback=None)
@ -50,15 +54,21 @@ class Client:
def add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
:param package: package properties
:param status: current package build status
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
"""
def get(self, base: Optional[str]) -> List[Tuple[Package, BuildStatus]]: # pylint: disable=no-self-use
"""
get package status
:param base: package base to get
:return: list of current package description and status if it has been found
Args:
base(Optional[str]): package base to get
Returns:
List[Tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
del base
return []
@ -66,67 +76,87 @@ class Client:
def get_internal(self) -> InternalStatus: # pylint: disable=no-self-use
"""
get internal service status
:return: current internal (web) service status
Returns:
InternalStatus: current internal (web) service status
"""
return InternalStatus()
def get_self(self) -> BuildStatus: # pylint: disable=no-self-use
"""
get ahriman status itself
:return: current ahriman status
Returns:
BuildStatus: current ahriman status
"""
return BuildStatus()
def remove(self, base: str) -> None:
"""
remove packages from watcher
:param base: package base to remove
Args:
base(str): 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
Args:
base(str): package base to update
status(BuildStatusEnum): current package build status
"""
def update_self(self, status: BuildStatusEnum) -> None:
"""
update ahriman status itself
:param status: current ahriman status
Args:
status(BuildStatusEnum): current ahriman status
"""
def set_building(self, base: str) -> None:
"""
set package status to building
:param base: package base to update
Args:
base(str): 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
Args:
base(str): 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
Args:
base(str): 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
Args:
package(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
Args:
package(Package): current package properties
"""
return self.add(package, BuildStatusEnum.Unknown)

View File

@ -32,20 +32,24 @@ from ahriman.models.package import Package
class Watcher:
"""
package status watcher
:ivar architecture: repository architecture
:ivar database: database instance
: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
Attributes:
architecture(str): repository architecture
database(SQLite): database instance
known(Dict[str, Tuple[Package, BuildStatus]]): list of known packages. For the most cases `packages` should be used instead
logger(logging.Logger): class logger
repository(Repository): repository object
status(BuildStatus): daemon status
"""
def __init__(self, architecture: str, configuration: Configuration, database: SQLite) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param database: database instance
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
database(SQLite): database instance
"""
self.logger = logging.getLogger("http")
@ -59,14 +63,25 @@ class Watcher:
@property
def packages(self) -> List[Tuple[Package, BuildStatus]]:
"""
:return: list of packages together with their statuses
get current known packages list
Returns:
List[Tuple[Package, BuildStatus]]: list of packages together with their statuses
"""
return list(self.known.values())
def get(self, base: str) -> Tuple[Package, BuildStatus]:
"""
get current package base build status
:return: package and its status
Args:
base(str): package base
Returns:
Tuple[Package, BuildStatus]: package and its status
Raises:
UnknownPackage: if no package found
"""
try:
return self.known[base]
@ -92,7 +107,9 @@ class Watcher:
def remove(self, package_base: str) -> None:
"""
remove package base from known list if any
:param package_base: package base
Args:
package_base(str): package base
"""
self.known.pop(package_base, None)
self.database.package_remove(package_base)
@ -100,9 +117,14 @@ class Watcher:
def update(self, package_base: str, status: BuildStatusEnum, package: Optional[Package]) -> None:
"""
update package status and description
:param package_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
Args:
package_base(str): package base to update
status(BuildStatusEnum): new build status
package(Optional[Package]): optional new package description. In case if not set current properties will be used
Raises:
UnknownPackage: if no package found
"""
if package is None:
try:
@ -116,6 +138,8 @@ class Watcher:
def update_self(self, status: BuildStatusEnum) -> None:
"""
update service status
:param status: new service status
Args:
status(BuildStatusEnum): new service status
"""
self.status = BuildStatus(status)

View File

@ -34,15 +34,19 @@ from ahriman.models.user import User
class WebClient(Client):
"""
build status reporter web client
:ivar address: address of the web service
:ivar logger: class logger
:ivar user: web service user descriptor
Attributes:
address(str): address of the web service
logger(logging.Logger): class logger
user(Optional[User]): web service user descriptor
"""
def __init__(self, configuration: Configuration) -> None:
"""
default constructor
:param configuration: configuration instance
Args:
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("http")
self.address = self.parse_address(configuration)
@ -56,21 +60,30 @@ class WebClient(Client):
@property
def _ahriman_url(self) -> str:
"""
:return: full url for web service for ahriman service itself
get url for the service status api
Returns:
str: full url for web service for ahriman service itself
"""
return f"{self.address}/status-api/v1/ahriman"
@property
def _login_url(self) -> str:
"""
:return: full url for web service to login
get url for the login api
Returns:
str: full url for web service to login
"""
return f"{self.address}/user-api/v1/login"
@property
def _status_url(self) -> str:
"""
:return: full url for web service for status
get url for the status api
Returns:
str: full url for web service for status
"""
return f"{self.address}/status-api/v1/status"
@ -78,8 +91,12 @@ class WebClient(Client):
def parse_address(configuration: Configuration) -> str:
"""
parse address from configuration
:param configuration: configuration instance
:return: valid http address
Args:
configuration(Configuration): configuration instance
Returns:
str: valid http address
"""
address = configuration.get("web", "address", fallback=None)
if not address:
@ -112,16 +129,22 @@ class WebClient(Client):
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
Args:
base(str, optional): package base to generate url (Default value = "")
Returns:
str: full url of web service for specific package base
"""
return f"{self.address}/status-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
Args:
package(Package): package properties
status(BuildStatusEnum): current package build status
"""
payload = {
"status": status.value,
@ -139,8 +162,12 @@ class WebClient(Client):
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
Args:
base(Optional[str]): package base to get
Returns:
List[Tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
try:
response = self.__session.get(self._package_url(base or ""))
@ -160,7 +187,9 @@ class WebClient(Client):
def get_internal(self) -> InternalStatus:
"""
get internal service status
:return: current internal (web) service status
Returns:
InternalStatus: current internal (web) service status
"""
try:
response = self.__session.get(self._status_url)
@ -177,7 +206,9 @@ class WebClient(Client):
def get_self(self) -> BuildStatus:
"""
get ahriman status itself
:return: current ahriman status
Returns:
BuildStatus: current ahriman status
"""
try:
response = self.__session.get(self._ahriman_url)
@ -194,7 +225,9 @@ class WebClient(Client):
def remove(self, base: str) -> None:
"""
remove packages from watcher
:param base: basename to remove
Args:
base(str): basename to remove
"""
try:
response = self.__session.delete(self._package_url(base))
@ -207,8 +240,10 @@ class WebClient(Client):
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
Args:
base(str): package base to update
status(BuildStatusEnum): current package build status
"""
payload = {"status": status.value}
@ -223,7 +258,9 @@ class WebClient(Client):
def update_self(self, status: BuildStatusEnum) -> None:
"""
update ahriman status itself
:param status: current ahriman status
Args:
status(BuildStatusEnum): current ahriman status
"""
payload = {"status": status.value}

View File

@ -30,15 +30,19 @@ from ahriman.models.package import Package
class Leaf:
"""
tree leaf implementation
:ivar dependencies: list of package dependencies
:ivar package: leaf package properties
Attributes:
dependencies(Set[str]): list of package dependencies
package(Package): leaf package properties
"""
def __init__(self, package: Package, dependencies: Set[str]) -> None:
"""
default constructor
:param package: package properties
:param dependencies: package dependencies
Args:
package(Package): package properties
dependencies(Set[str]): package dependencies
"""
self.package = package
self.dependencies = dependencies
@ -46,7 +50,10 @@ class Leaf:
@property
def items(self) -> Iterable[str]:
"""
:return: packages containing in this leaf
extract all packages from the leaf
Returns:
Iterable[str]: packages containing in this leaf
"""
return self.package.packages.keys()
@ -54,9 +61,13 @@ class Leaf:
def load(cls: Type[Leaf], package: Package, database: SQLite) -> Leaf:
"""
load leaf from package with dependencies
:param package: package properties
:param database: database instance
:return: loaded class
Args:
package(Package): package properties
database(SQLite): database instance
Returns:
Leaf: loaded class
"""
with tmpdir() as clone_dir:
Sources.load(clone_dir, package.git_url, database.patches_get(package.base))
@ -66,8 +77,12 @@ class Leaf:
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
Args:
packages(Iterable[Leaf]): list of known leaves
Returns:
bool: True if any of packages is dependency of the leaf, False otherwise
"""
for leaf in packages:
if self.dependencies.intersection(leaf.items):
@ -78,13 +93,17 @@ class Leaf:
class Tree:
"""
dependency tree implementation
:ivar leaves: list of tree leaves
Attributes:
leaves[List[Leaf]): list of tree leaves
"""
def __init__(self, leaves: List[Leaf]) -> None:
"""
default constructor
:param leaves: leaves to build the tree
Args:
leaves(List[Leaf]): leaves to build the tree
"""
self.leaves = leaves
@ -92,16 +111,22 @@ class Tree:
def load(cls: Type[Tree], packages: Iterable[Package], database: SQLite) -> Tree:
"""
load tree from packages
:param packages: packages list
:param database: database instance
:return: loaded class
Args:
packages(Iterable[Package]): packages list
database(SQLite): database instance
Returns:
Tree: loaded class
"""
return cls([Leaf.load(package, database) 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
Returns:
List[List[Package]]: list of packages lists
"""
result: List[List[Package]] = []

View File

@ -32,26 +32,32 @@ from ahriman.models.package import Package
class Github(HttpUpload):
"""
upload files to github releases
:ivar gh_owner: github repository owner
:ivar gh_repository: github repository name
Attributes:
github_owner(str): github repository owner
github_repository(str): github repository name
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
HttpUpload.__init__(self, architecture, configuration, section)
self.gh_owner = configuration.get(section, "owner")
self.gh_repository = configuration.get(section, "repository")
self.github_owner = configuration.get(section, "owner")
self.github_repository = configuration.get(section, "repository")
def asset_remove(self, release: Dict[str, Any], name: str) -> None:
"""
remove asset from the release by name
:param release: release object
:param name: asset name
Args:
release(Dict[str, Any]): release object
name(str): asset name
"""
try:
asset = next(asset for asset in release["assets"] if asset["name"] == name)
@ -62,8 +68,10 @@ class Github(HttpUpload):
def asset_upload(self, release: Dict[str, Any], path: Path) -> None:
"""
upload asset to the release
:param release: release object
:param path: path to local file
Args:
release(Dict[str, Any]): release object
path(Path): path to local file
"""
exists = any(path.name == asset["name"] for asset in release["assets"])
if exists:
@ -76,8 +84,12 @@ class Github(HttpUpload):
def get_local_files(self, path: Path) -> Dict[Path, str]:
"""
get all local files and their calculated checksums
:param path: local path to sync
:return: map of path objects to its checksum
Args:
path(Path): local path to sync
Returns:
Dict[Path, str]: map of path objects to its checksum
"""
return {
local_file: self.calculate_hash(local_file)
@ -87,9 +99,11 @@ class Github(HttpUpload):
def files_remove(self, release: Dict[str, Any], local_files: Dict[Path, str], remote_files: Dict[str, str]) -> None:
"""
remove files from github
:param release: release object
:param local_files: map of local file paths to its checksum
:param remote_files: map of the remote files and its checksum
Args:
release(Dict[str, Any]): release object
local_files(Dict[Path, str]): map of local file paths to its checksum
remote_files(Dict[str, str]): map of the remote files and its checksum
"""
local_filenames = {local_file.name for local_file in local_files}
for remote_file in remote_files:
@ -100,9 +114,11 @@ class Github(HttpUpload):
def files_upload(self, release: Dict[str, Any], local_files: Dict[Path, str], remote_files: Dict[str, str]) -> None:
"""
upload files to github
:param release: release object
:param local_files: map of local file paths to its checksum
:param remote_files: map of the remote files and its checksum
Args:
release(Dict[str, Any]): release object
local_files(Dict[Path, str]): map of local file paths to its checksum
remote_files(Dict[str, str]): map of the remote files and its checksum
"""
for local_file, checksum in local_files.items():
remote_checksum = remote_files.get(local_file.name)
@ -113,22 +129,25 @@ class Github(HttpUpload):
def release_create(self) -> Dict[str, Any]:
"""
create empty release
:return: github API release object for the new release
Returns:
Dict[str, Any]: github API release object for the new release
"""
response = self._request("POST", f"https://api.github.com/repos/{self.gh_owner}/{self.gh_repository}/releases",
json={"tag_name": self.architecture, "name": self.architecture})
url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases"
response = self._request("POST", url, json={"tag_name": self.architecture, "name": self.architecture})
release: Dict[str, Any] = response.json()
return release
def release_get(self) -> Optional[Dict[str, Any]]:
"""
get release object if any
:return: github API release object if release found and None otherwise
Returns:
Optional[Dict[str, Any]]: github API release object if release found and None otherwise
"""
url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.architecture}"
try:
response = self._request(
"GET",
f"https://api.github.com/repos/{self.gh_owner}/{self.gh_repository}/releases/tags/{self.architecture}")
response = self._request("GET", url)
release: Dict[str, Any] = response.json()
return release
except requests.HTTPError as e:
@ -140,16 +159,20 @@ class Github(HttpUpload):
def release_update(self, release: Dict[str, Any], body: str) -> None:
"""
update release
:param release: release object
:param body: new release body
Args:
release(Dict[str, Any]): release object
body(str): new release body
"""
self._request("POST", release["url"], json={"body": body})
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
"""
sync data to remote server
:param path: local path to sync
:param built_packages: list of packages which has just been built
Args:
path(Path): local path to sync
built_packages(Iterable[Package]): list of packages which has just been built
"""
release = self.release_get()
if release is None:

View File

@ -31,15 +31,19 @@ from ahriman.core.util import exception_response_text
class HttpUpload(Upload):
"""
helper for the http based uploads
:ivar auth: HTTP auth object
Attributes:
auth(Tuple[str, str]): HTTP auth object
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: configuration section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): configuration section name
"""
Upload.__init__(self, architecture, configuration)
password = configuration.get(section, "password")
@ -50,8 +54,12 @@ class HttpUpload(Upload):
def calculate_hash(path: Path) -> str:
"""
calculate file checksum
:param path: path to local file
:return: calculated checksum of the file
Args:
path(Path): path to local file
Returns:
str: calculated checksum of the file
"""
with path.open("rb") as local_file:
md5 = hashlib.md5(local_file.read()) # nosec
@ -61,8 +69,12 @@ class HttpUpload(Upload):
def get_body(local_files: Dict[Path, str]) -> str:
"""
generate release body from the checksums as returned from HttpUpload.get_hashes method
:param local_files: map of the paths to its checksum
:return: body to be inserted into release
Args:
local_files(Dict[Path, str]): map of the paths to its checksum
Returns:
str: body to be inserted into release
"""
return "\n".join(f"{file.name} {md5}" for file, md5 in sorted(local_files.items()))
@ -70,8 +82,12 @@ class HttpUpload(Upload):
def get_hashes(body: str) -> Dict[str, str]:
"""
get checksums of the content from the repository
:param body: release string body object
:return: map of the filename to its checksum as it is written in body
Args:
body(str): release string body object
Returns:
Dict[str, str]: map of the filename to its checksum as it is written in body
"""
files = {}
for line in body.splitlines():
@ -82,10 +98,14 @@ class HttpUpload(Upload):
def _request(self, method: str, url: str, **kwargs: Any) -> requests.Response:
"""
request wrapper
:param method: request method
:param url: request url
:param kwargs: request parameters to be passed as is
:return: request response object
Args:
method(str): request method
url(str): request url
**kwargs(Any): request parameters to be passed as is
Returns:
requests.Response: request response object
"""
try:
response = requests.request(method, url, auth=self.auth, **kwargs)

View File

@ -29,8 +29,10 @@ from ahriman.models.package import Package
class Rsync(Upload):
"""
rsync wrapper
:ivar command: command arguments for sync
:ivar remote: remote address to sync
Attributes:
command(List[str]): command arguments for sync
remote(str): remote address to sync
"""
_check_output = check_output
@ -38,9 +40,11 @@ class Rsync(Upload):
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
:param section: settings section name
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Upload.__init__(self, architecture, configuration)
self.command = configuration.getlist(section, "command")
@ -49,7 +53,9 @@ class Rsync(Upload):
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
"""
sync data to remote server
:param path: local path to sync
:param built_packages: list of packages which has just been built
Args:
path(Path): local path to sync
built_packages(Iterable[Package]): list of packages which has just been built
"""
Rsync._check_output(*self.command, str(path), self.remote, exception=None, logger=self.logger)

View File

@ -32,16 +32,21 @@ from ahriman.models.package import Package
class S3(Upload):
"""
aws-cli wrapper
:ivar bucket: boto3 S3 bucket object
:ivar chunk_size: chunk size for calculating checksums
boto3 wrapper
Attributes
bucket(Any): boto3 S3 bucket object
chunk_size(int): chunk size for calculating checksums
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Upload.__init__(self, architecture, configuration)
self.bucket = self.get_bucket(configuration, section)
@ -53,9 +58,13 @@ class S3(Upload):
calculate amazon s3 etag
credits to https://teppen.io/2018/10/23/aws_s3_verify_etags/
For this method we have to define nosec because it is out of any security context and provided by AWS
:param path: path to local file
:param chunk_size: read chunk size, which depends on client settings
:return: calculated entity tag for local file
Args:
path(Path): path to local file
chunk_size(int): read chunk size, which depends on client settings
Returns:
str: calculated entity tag for local file
"""
md5s = []
with path.open("rb") as local_file:
@ -73,9 +82,13 @@ class S3(Upload):
def get_bucket(configuration: Configuration, section: str) -> Any:
"""
create resource client from configuration
:param configuration: configuration instance
:param section: settings section name
:return: amazon client
Args:
configuration(Configuration): configuration instance
section(str): settings section name
Returns:
Any: amazon client
"""
client = boto3.resource(service_name="s3",
region_name=configuration.get(section, "region"),
@ -87,8 +100,10 @@ class S3(Upload):
def files_remove(local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
"""
remove files which have been removed locally
:param local_files: map of local path object to its checksum
:param remote_objects: map of remote path object to the remote s3 object
Args:
local_files(Dict[Path, str]): map of local path object to its checksum
remote_objects(Dict[Path, Any]): map of remote path object to the remote s3 object
"""
for local_file, remote_object in remote_objects.items():
if local_file in local_files:
@ -98,9 +113,11 @@ class S3(Upload):
def files_upload(self, path: Path, local_files: Dict[Path, str], remote_objects: Dict[Path, Any]) -> None:
"""
upload changed files to s3
:param path: local path to sync
:param local_files: map of local path object to its checksum
:param remote_objects: map of remote path object to the remote s3 object
Args:
path(Path): local path to sync
local_files(Dict[Path, str]): map of local path object to its checksum
remote_objects(Dict[Path, Any]): map of remote path object to the remote s3 object
"""
for local_file, checksum in local_files.items():
remote_object = remote_objects.get(local_file)
@ -119,8 +136,12 @@ class S3(Upload):
def get_local_files(self, path: Path) -> Dict[Path, str]:
"""
get all local files and their calculated checksums
:param path: local path to sync
:return: map of path object to its checksum
Args:
path(Path): local path to sync
Returns:
Dict[Path, str]: map of path object to its checksum
"""
return {
local_file.relative_to(path): self.calculate_etag(local_file, self.chunk_size)
@ -130,7 +151,9 @@ class S3(Upload):
def get_remote_objects(self) -> Dict[Path, Any]:
"""
get all remote objects and their checksums
:return: map of path object to the remote s3 object
Returns:
Dict[Path, Any]: map of path object to the remote s3 object
"""
objects = self.bucket.objects.filter(Prefix=self.architecture)
return {Path(item.key).relative_to(self.architecture): item for item in objects}
@ -138,8 +161,10 @@ class S3(Upload):
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
"""
sync data to remote server
:param path: local path to sync
:param built_packages: list of packages which has just been built
Args:
path(Path): local path to sync
built_packages(Iterable[Package]): list of packages which has just been built
"""
remote_objects = self.get_remote_objects()
local_files = self.get_local_files(path)

View File

@ -33,16 +33,20 @@ from ahriman.models.upload_settings import UploadSettings
class Upload:
"""
base remote sync class
:ivar architecture: repository architecture
:ivar configuration: configuration instance
:ivar logger: application logger
Attributes:
architecture(str): repository architecture
configuration(Configuration): configuration instance
logger(logging.Logger): application logger
"""
def __init__(self, architecture: str, configuration: Configuration) -> None:
"""
default constructor
:param architecture: repository architecture
:param configuration: configuration instance
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.logger = logging.getLogger("root")
self.architecture = architecture
@ -52,10 +56,14 @@ class Upload:
def load(cls: Type[Upload], architecture: str, configuration: Configuration, target: str) -> Upload:
"""
load client from settings
:param architecture: repository architecture
:param configuration: configuration instance
:param target: target to run sync (e.g. s3)
:return: client according to current settings
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
target(str): target to run sync (e.g. s3)
Returns:
Upload: client according to current settings
"""
section, provider_name = configuration.gettype(target, architecture)
provider = UploadSettings.from_option(provider_name)
@ -73,8 +81,13 @@ class Upload:
def run(self, path: Path, built_packages: Iterable[Package]) -> None:
"""
run remote sync
:param path: local path to sync
:param built_packages: list of packages which has just been built
Args:
path(Path): local path to sync
built_packages(Iterable[Package]): list of packages which has just been built
Raises:
SyncFailed: in case of any synchronization unmatched exception
"""
try:
self.sync(path, built_packages)
@ -85,6 +98,8 @@ class Upload:
def sync(self, path: Path, built_packages: Iterable[Package]) -> None:
"""
sync data to remote server
:param path: local path to sync
:param built_packages: list of packages which has just been built
Args:
path(Path): local path to sync
built_packages(Iterable[Package]): list of packages which has just been built
"""

View File

@ -37,13 +37,20 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
input_data: Optional[str] = None, logger: Optional[Logger] = None, user: Optional[int] = 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 input_data: data which will be written to command stdin
:param logger: logger to log command result if required
:param user: run process as specified user
:return: command output
Args:
*args(str): command line arguments
exception(Optional[Exception]): exception which has to be reraised instead of default subprocess exception
cwd(Optional[Path], optional): current working directory (Default value = None)
input_data(Optional[str], optional): data which will be written to command stdin (Default value = None)
logger(Optional[Logger], optional): logger to log command result if required (Default value = None)
user(Optional[int], optional): run process as specified user (Default value = None)
Returns:
str: command output
Raises:
subprocess.CalledProcessError: if subprocess ended with status code different from 0 and no exception supplied
"""
def log(single: str) -> None:
if logger is not None:
@ -83,8 +90,13 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
def check_user(paths: RepositoryPaths, unsafe: bool) -> None:
"""
check if current user is the owner of the root
:param paths: repository paths object
:param unsafe: if set no user check will be performed before path creation
Args:
paths(RepositoryPaths): repository paths object
unsafe(bool): if set no user check will be performed before path creation
Raises:
UnsafeRun: if root uid differs from current uid and check is enabled
"""
if not paths.root.exists():
return # no directory found, skip check
@ -99,8 +111,12 @@ def check_user(paths: RepositoryPaths, unsafe: bool) -> None:
def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
"""
safe response exception text generation
:param exception: exception raised
:return: text of the response if it is not None and empty string otherwise
Args:
exception(requests.exceptions.HTTPError): exception raised
Returns:
str: text of the response if it is not None and empty string otherwise
"""
result: str = exception.response.text if exception.response is not None else ""
return result
@ -109,9 +125,13 @@ def exception_response_text(exception: requests.exceptions.HTTPError) -> str:
def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str, Any]:
"""
filter json object by fields used for json-to-object conversion
:param source: raw json object
:param known_fields: list of fields which have to be known for the target object
:return: json object without unknown and empty fields
Args:
source(Dict[str, Any]): raw json object
known_fields(Iterable[str]): list of fields which have to be known for the target object
Returns:
Dict[str, Any]: json object without unknown and empty fields
"""
return {key: value for key, value in source.items() if key in known_fields and value is not None}
@ -119,10 +139,14 @@ def filter_json(source: Dict[str, Any], known_fields: Iterable[str]) -> Dict[str
def full_version(epoch: Union[str, int, None], 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 (arch linux specific)
:return: generated version
Args:
epoch(Union[str, int, None]): package epoch if any
pkgver(str): package version
pkgrel(str): package release version (arch linux specific)
Returns:
str: generated version
"""
prefix = f"{epoch}:" if epoch else ""
return f"{prefix}{pkgver}-{pkgrel}"
@ -131,8 +155,12 @@ def full_version(epoch: Union[str, int, None], pkgver: str, pkgrel: str) -> str:
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
Args:
filename(Path): name of file to check
Returns:
bool: True in case if name contains `.pkg.` and not signature, False otherwise
"""
name = filename.name
return ".pkg." in name and not name.endswith(".sig")
@ -141,8 +169,12 @@ def package_like(filename: Path) -> bool:
def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -> str:
"""
convert datetime object to string
:param timestamp: datetime to convert
:return: pretty printable datetime as string
Args:
timestamp(Optional[Union[datetime.datetime, float, int]]): datetime to convert
Returns:
str: pretty printable datetime as string
"""
if timestamp is None:
return ""
@ -154,9 +186,16 @@ def pretty_datetime(timestamp: Optional[Union[datetime.datetime, float, int]]) -
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
Args:
size(Optional[float]): size to convert
level(int, optional): represents current units, 0 is B, 1 is KiB etc (Default value = 0)
Returns:
str: pretty printable size as string
Raises:
InvalidOption: if size is more than 1TiB
"""
def str_level() -> str:
if level == 0:
@ -180,7 +219,9 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
def tmpdir() -> Generator[Path, None, None]:
"""
wrapper for tempfile to remove directory after all
:return: path to the created directory
Yields:
Path: path to the created directory
"""
path = Path(tempfile.mkdtemp())
try:
@ -193,8 +234,12 @@ def walk(directory_path: Path) -> Generator[Path, None, None]:
"""
list all file paths in given directory
Credits to https://stackoverflow.com/a/64915960
:param directory_path: root directory path
:return: all found files in given directory with full path
Args:
directory_path(Path): root directory path
Yields:
Path: all found files in given directory with full path
"""
for element in directory_path.iterdir():
if element.is_dir():

View File

@ -23,9 +23,11 @@ from enum import Enum
class Action(Enum):
"""
base action enumeration
:cvar List: list available values
:cvar Remove: remove everything from local storage
:cvar Update: update local storage or add to
Attributes:
List(Action): (class attribute) list available values
Remove(Action): (class attribute) remove everything from local storage
Update(Action): (class attribute) update local storage or add to
"""
List = "list"

View File

@ -32,26 +32,28 @@ from ahriman.core.util import filter_json, full_version
class AURPackage:
"""
AUR package descriptor
:ivar id: package ID
:ivar name: package name
:ivar package_base_id: package base ID
:ivar version: package base version
:ivar description: package base description
:ivar url: package upstream URL
:ivar num_votes: number of votes for the package
:ivar polularity: package popularity
:ivar out_of_date: package out of date timestamp if any
:ivar maintainer: package maintainer
:ivar first_submitted: timestamp of the first package submission
:ivar last_modified: timestamp of the last package submission
:ivar url_path: AUR package path
:ivar depends: list of package dependencies
:ivar make_depends: list of package make dependencies
:ivar opt_depends: list of package optional dependencies
:ivar conflicts: conflicts list for the package
:ivar provides: list of packages which this package provides
:ivar license: list of package licenses
:ivar keywords: list of package keywords
Attributes:
id(int): package ID
name(str): package name
package_base_id(int): package base ID
version(str): package base version
description(str): package base description
url(Optional[str]): package upstream URL
num_votes(int): number of votes for the package
polularity(float): package popularity
out_of_date(Optional[datetime.datetime]): package out of date timestamp if any
maintainer(Optional[str]): package maintainer
first_submitted(datetime.datetime): timestamp of the first package submission
last_modified(datetime.datetime): timestamp of the last package submission
url_path(str): AUR package path
depends(List[str]): list of package dependencies
make_depends(List[str]): list of package make dependencies
opt_depends(List[str]): list of package optional dependencies
conflicts(List[str]): conflicts list for the package
provides(List[str]): list of packages which this package provides
license(List[str]): list of package licenses
keywords(List[str]): list of package keywords
"""
id: int
@ -80,8 +82,12 @@ class AURPackage:
def from_json(cls: Type[AURPackage], dump: Dict[str, Any]) -> AURPackage:
"""
construct package descriptor from RPC properties
:param dump: json dump body
:return: AUR package descriptor
Args:
dump(Dict[str, Any]): json dump body
Returns:
AURPackage: AUR package descriptor
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
@ -92,8 +98,12 @@ class AURPackage:
def from_repo(cls: Type[AURPackage], dump: Dict[str, Any]) -> AURPackage:
"""
construct package descriptor from official repository RPC properties
:param dump: json dump body
:return: AUR package descriptor
Args:
dump(Dict[str, Any]): json dump body
Returns:
AURPackage: AUR package descriptor
"""
return cls(
id=0,
@ -125,8 +135,12 @@ class AURPackage:
def convert(descriptor: Dict[str, Any]) -> Dict[str, Any]:
"""
covert AUR RPC key names to package keys
:param descriptor: RPC package descriptor
:return: package descriptor with names converted to snake case
Args:
descriptor(Dict[str, Any]): RPC package descriptor
Returns:
Dict[str, Any]: package descriptor with names converted to snake case
"""
identity_mapper: Callable[[Any], Any] = lambda value: value
value_mapper: Dict[str, Callable[[Any], Any]] = {

View File

@ -22,15 +22,15 @@ from __future__ import annotations
from enum import Enum
from typing import Type
from ahriman.core.exceptions import InvalidOption
class AuthSettings(Enum):
"""
web authorization type
:cvar Disabled: authorization is disabled
:cvar Configuration: configuration based authorization
:cvar OAuth: OAuth based provider
Attributes:
Disabled(AuthSettings): (class attribute) authorization is disabled
Configuration(AuthSettings): (class attribute) configuration based authorization
OAuth(AuthSettings): (class attribute) OAuth based provider
"""
Disabled = "disabled"
@ -41,21 +41,26 @@ class AuthSettings(Enum):
def from_option(cls: Type[AuthSettings], value: str) -> AuthSettings:
"""
construct value from configuration
:param value: configuration value
:return: parsed value
Args:
value(str): configuration value
Returns:
AuthSettings: parsed value
"""
if value.lower() in ("disabled", "no"):
return cls.Disabled
if value.lower() in ("configuration", "mapping"):
return cls.Configuration
if value.lower() in ('oauth', 'oauth2'):
return cls.OAuth
raise InvalidOption(value)
return cls.Disabled
@property
def is_enabled(self) -> bool:
"""
:return: False in case if authorization is disabled and True otherwise
get enabled flag
Returns:
bool: False in case if authorization is disabled and True otherwise
"""
if self == AuthSettings.Disabled:
return False

View File

@ -31,11 +31,13 @@ from ahriman.core.util import filter_json, 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
Attributes:
Unknown(BuildStatusEnum): (class attribute) build status is unknown
Pending(BuildStatusEnum): (class attribute) package is out-of-dated and will be built soon
Building(BuildStatusEnum): (class attribute) package is building right now
Failed(BuildStatusEnum): (class attribute) package build failed
Success(BuildStatusEnum): (class attribute) package has been built without errors
"""
Unknown = "unknown"
@ -47,7 +49,9 @@ class BuildStatusEnum(Enum):
def badges_color(self) -> str:
"""
convert itself to shield.io badges color
:return: shields.io color
Returns:
str: shields.io color
"""
if self == BuildStatusEnum.Pending:
return "yellow"
@ -62,7 +66,9 @@ class BuildStatusEnum(Enum):
def bootstrap_color(self) -> str:
"""
converts itself to bootstrap color
:return: bootstrap color
Returns:
str: bootstrap color
"""
if self == BuildStatusEnum.Pending:
return "warning"
@ -79,8 +85,10 @@ class BuildStatusEnum(Enum):
class BuildStatus:
"""
build status holder
:ivar status: build status
:ivar timestamp: build status update time
Attributes:
status(BuildStatusEnum): build status
timestamp(int): build status update time
"""
status: BuildStatusEnum = BuildStatusEnum.Unknown
@ -96,8 +104,12 @@ class BuildStatus:
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
Args:
dump(Dict[str, Any]): json dump body
Returns:
BuildStatus: status properties
"""
known_fields = [pair.name for pair in fields(cls)]
return cls(**filter_json(dump, known_fields))
@ -105,14 +117,18 @@ class BuildStatus:
def pretty_print(self) -> str:
"""
generate pretty string representation
:return: print-friendly string
Returns:
str: print-friendly string
"""
return f"{self.status.value} ({pretty_datetime(self.timestamp)})"
def view(self) -> Dict[str, Any]:
"""
generate json status view
:return: json-friendly dictionary
Returns:
Dict[str, Any]: json-friendly dictionary
"""
return {
"status": self.status.value,

View File

@ -31,12 +31,14 @@ from ahriman.models.package import Package
class Counters:
"""
package counters
:ivar total: total packages count
:ivar unknown: packages in unknown status count
:ivar pending: packages in pending status count
:ivar building: packages in building status count
:ivar failed: packages in failed status count
:ivar success: packages in success status count
Attributes:
total(int): total packages count
unknown(int): packages in unknown status count
pending(int): packages in pending status count
building(int): packages in building status count
failed(int): packages in failed status count
success(int): packages in success status count
"""
total: int
@ -50,8 +52,12 @@ class Counters:
def from_json(cls: Type[Counters], dump: Dict[str, Any]) -> Counters:
"""
construct counters from json dump
:param dump: json dump body
:return: status counters
Args:
dump(Dict[str, Any]): json dump body
Returns:
Counters: status counters
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
@ -61,8 +67,12 @@ class Counters:
def from_packages(cls: Type[Counters], packages: List[Tuple[Package, BuildStatus]]) -> Counters:
"""
construct counters from packages statuses
:param packages: list of package and their status as per watcher property
:return: status counters
Args:
packages(List[Tuple[Package, BuildStatus]]): list of package and their status as per watcher property
Returns:
Counters: status counters
"""
per_status = {"total": len(packages)}
for _, status in packages:

View File

@ -29,10 +29,12 @@ from ahriman.models.counters import Counters
class InternalStatus:
"""
internal server status
:ivar architecture: repository architecture
:ivar packages: packages statuses counter object
:ivar repository: repository name
:ivar version: service version
Attributes:
architecture(Optional[str]): repository architecture
packages(Counters): packages statuses counter object
repository(Optional[str]): repository name
version(Optional[str]): service version
"""
architecture: Optional[str] = None
@ -44,8 +46,12 @@ class InternalStatus:
def from_json(cls: Type[InternalStatus], dump: Dict[str, Any]) -> InternalStatus:
"""
construct internal status from json dump
:param dump: json dump body
:return: internal status
Args:
dump(Dict[str, Any]): json dump body
Returns:
InternalStatus: internal status
"""
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
return cls(architecture=dump.get("architecture"),
@ -56,6 +62,8 @@ class InternalStatus:
def view(self) -> Dict[str, Any]:
"""
generate json status view
:return: json-friendly dictionary
Returns:
Dict[str, Any]: json-friendly dictionary
"""
return asdict(self)

View File

@ -25,9 +25,11 @@ from typing import List
class Migration:
"""
migration implementation
:ivar index: migration position
:ivar name: migration name
:ivar steps: migration steps
Attributes:
index(int): migration position
name(str): migration name
steps(List[str]): migration steps
"""
index: int

View File

@ -26,8 +26,10 @@ from ahriman.core.exceptions import MigrationError
class MigrationResult:
"""
migration result implementation model
:ivar old_version: old schema version before migrations
:ivar new_version: new schema version after migrations
Attributes:
old_version(int): old schema version before migrations
new_version(int): new schema version after migrations
"""
old_version: int
@ -36,7 +38,10 @@ class MigrationResult:
@property
def is_outdated(self) -> bool:
"""
:return: True in case if it requires migrations and False otherwise
check migration and check if there are pending migrations
Returns:
bool: True in case if it requires migrations and False otherwise
"""
self.validate()
return self.new_version > self.old_version
@ -44,6 +49,9 @@ class MigrationResult:
def validate(self) -> None:
"""
perform version validation
Raises:
MigrationError: if old version is newer than new one or negative
"""
if self.old_version < 0 or self.old_version > self.new_version:
raise MigrationError(f"Invalid current schema version, expected less or equal to {self.new_version}, "

View File

@ -42,10 +42,12 @@ from ahriman.models.repository_paths import RepositoryPaths
class Package:
"""
package properties representation
: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
Attributes:
aur_url(str): AUR root url
base(str): package base name
packages(Dict[str, PackageDescription): map of package names to their properties. Filled only on load from archive
version(str): package full version
"""
base: str
@ -58,35 +60,50 @@ class Package:
@property
def depends(self) -> List[str]:
"""
:return: sum of dependencies per arch package
get package base dependencies
Returns:
List[str]: sum of dependencies per each package
"""
return sorted(set(sum([package.depends for package in self.packages.values()], start=[])))
@property
def git_url(self) -> str:
"""
:return: package git url to clone
get git clone url
Returns:
str: package git url to clone
"""
return f"{self.aur_url}/{self.base}.git"
@property
def groups(self) -> List[str]:
"""
:return: sum of groups per each package
get package base groups
Returns:
List[str]: sum of groups per each package
"""
return sorted(set(sum([package.groups for package in self.packages.values()], start=[])))
@property
def is_single_package(self) -> bool:
"""
:return: true in case if this base has only one package with the same name
is it possible to transform package base to single package or not
Returns:
bool: 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
get VCS flag based on the package base
Returns:
bool: True in case if package base looks like VCS package and False otherwise
"""
return self.base.endswith("-bzr") \
or self.base.endswith("-csv")\
@ -98,14 +115,20 @@ class Package:
@property
def licenses(self) -> List[str]:
"""
:return: sum of licenses per each package
get package base licenses
Returns:
List[str]: sum of licenses per each package
"""
return sorted(set(sum([package.licenses for package in self.packages.values()], start=[])))
@property
def web_url(self) -> str:
"""
:return: package AUR url
get package url which can be used to see package in web
Returns:
str: package AUR url
"""
return f"{self.aur_url}/packages/{self.base}"
@ -113,10 +136,14 @@ class 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
Args:
path(Path): path to package archive
pacman(Pacman): alpm wrapper instance
aur_url(str): AUR root url
Returns:
Package: package properties
"""
package = pacman.handle.load_pkg(str(path))
return cls(package.base, package.version, aur_url,
@ -126,9 +153,13 @@ class Package:
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
Args:
name(str): package name (either base or normal name)
aur_url(str): AUR root url
Returns:
Package: package properties
"""
package = AUR.info(name)
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
@ -137,9 +168,16 @@ class 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
Args:
path(Path): path to package sources directory
aur_url(str): AUR root url
Returns:
Package: package properties
Raises:
InvalidPackageInfo: if there are parsing errors
"""
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
if errors:
@ -153,8 +191,12 @@ class Package:
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
Args:
dump(Dict[str, Any]): json dump body
Returns:
Package: package properties
"""
packages = {
key: PackageDescription.from_json(value)
@ -170,9 +212,13 @@ class Package:
def from_official(cls: Type[Package], name: str, aur_url: str) -> Package:
"""
construct package properties from official repository page
:param name: package name (either base or normal name)
:param aur_url: AUR root url
:return: package properties
Args:
name(str): package name (either base or normal name)
aur_url(str): AUR root url
Returns:
Package: package properties
"""
package = Official.info(name)
return cls(package.package_base, package.version, aur_url, {package.name: PackageDescription()})
@ -181,11 +227,18 @@ class Package:
def load(cls: Type[Package], package: str, source: PackageSource, pacman: Pacman, aur_url: str) -> Package:
"""
package constructor from available sources
:param package: one of path to sources directory, path to archive or package name/base
:param source: source of the package required to define the load method
:param pacman: alpm wrapper instance (required to load from archive)
:param aur_url: AUR root url
:return: package properties
Args:
package(str): one of path to sources directory, path to archive or package name/base
source(PackageSource): source of the package required to define the load method
pacman(Pacman): alpm wrapper instance (required to load from archive)
aur_url(str): AUR root url
Returns:
Package: package properties
Raises:
InvalidPackageInfo: if supplied package source is not valid
"""
try:
resolved_source = source.resolve(package)
@ -207,8 +260,15 @@ class Package:
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
Args:
path(Path): path to package sources directory
Returns:
Set[str]: list of package dependencies including makedepends array, but excluding packages from this base
Raises:
InvalidPackageInfo: if there are parsing errors
"""
# additional function to remove versions from dependencies
def extract_packages(raw_packages_list: List[str]) -> Set[str]:
@ -234,8 +294,15 @@ class Package:
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
Args:
paths(RepositoryPaths): repository paths instance
Returns:
str: package version if package is not VCS and current version according to VCS otherwise
Raises:
InvalidPackageInfo: if there are parsing errors
"""
if not self.is_vcs:
return self.version
@ -265,9 +332,13 @@ class Package:
def full_depends(self, pacman: Pacman, packages: Iterable[Package]) -> List[str]:
"""
generate full dependencies list including transitive dependencies
:param pacman: alpm wrapper instance
:param packages: repository package list
:return: all dependencies of the package
Args:
pacman(Pacman): alpm wrapper instance
packages(Iterable[Package]): repository package list
Returns:
List[str]: all dependencies of the package
"""
dependencies = {}
# load own package dependencies
@ -295,10 +366,14 @@ class Package:
def is_outdated(self, remote: Package, paths: RepositoryPaths, calculate_version: bool = True) -> 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
:param calculate_version: expand version to actual value (by calculating git versions)
:return: True if the package is out-of-dated and False otherwise
Args:
remote(Package): package properties from remote source
paths(RepositoryPaths): repository paths instance. Required for VCS packages cache
calculate_version(bool, optional): expand version to actual value (by calculating git versions) (Default value = True)
Returns:
bool: True if the package is out-of-dated and False otherwise
"""
remote_version = remote.actual_version(paths) if calculate_version else remote.version
result: int = vercmp(self.version, remote_version)
@ -307,7 +382,9 @@ class Package:
def pretty_print(self) -> str:
"""
generate pretty string representation
:return: print-friendly string
Returns:
str: print-friendly string
"""
details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})"""
return f"{self.base}{details}"
@ -315,6 +392,8 @@ class Package:
def view(self) -> Dict[str, Any]:
"""
generate json package view
:return: json-friendly dictionary
Returns:
Dict[str, Any]: json-friendly dictionary
"""
return asdict(self)

View File

@ -31,17 +31,19 @@ from ahriman.core.util import filter_json
class PackageDescription:
"""
package specific properties
:ivar architecture: package architecture
:ivar archive_size: package archive size
:ivar build_date: package build date
:ivar depends: package dependencies list
:ivar description: package description
:ivar filename: package archive name
:ivar groups: package groups
:ivar installed_size: package installed size
:ivar licenses: package licenses list
:ivar provides: list of provided packages
:ivar url: package url
Attributes:
architecture(Optional[str]): package architecture
archive_size(Optional[int]): package archive size
build_date(Optional[int]): package build date
depends(List[str]): package dependencies list
description(Optional[str]): package description
filename(Optional[str]): package archive name
groups(List[str]): package groups
installed_size(Optional[int]): package installed size
licenses(List[str]): package licenses list
provides(List[str]): list of provided packages
url(Optional[str]): package url
"""
architecture: Optional[str] = None
@ -59,7 +61,10 @@ class PackageDescription:
@property
def filepath(self) -> Optional[Path]:
"""
:return: path object for current filename
wrapper for filename, convert it to Path object
Returns:
Optional[Path]: path object for current filename
"""
return Path(self.filename) if self.filename is not None else None
@ -67,8 +72,12 @@ class PackageDescription:
def from_json(cls: Type[PackageDescription], dump: Dict[str, Any]) -> PackageDescription:
"""
construct package properties from json dump
:param dump: json dump body
:return: package properties
Args:
dump(Dict[str, Any]): json dump body
Returns:
PackageDescription: package properties
"""
# filter to only known fields
known_fields = [pair.name for pair in fields(cls)]
@ -78,9 +87,13 @@ class PackageDescription:
def from_package(cls: Type[PackageDescription], package: Package, path: Path) -> PackageDescription:
"""
construct class from alpm package class
:param package: alpm generated object
:param path: path to package archive
:return: package properties based on tarball
Args:
package(Package): alpm generated object
path(Path): path to package archive
Returns:
PackageDescription: package properties based on tarball
"""
return cls(
architecture=package.arch,
@ -98,6 +111,8 @@ class PackageDescription:
def view(self) -> Dict[str, Any]:
"""
generate json package view
:return: json-friendly dictionary
Returns:
Dict[str, Any]: json-friendly dictionary
"""
return asdict(self)

View File

@ -29,13 +29,15 @@ from ahriman.core.util import package_like
class PackageSource(Enum):
"""
package source for addition enumeration
:cvar Auto: automatically determine type of the source
:cvar Archive: source is a package archive
:cvar AUR: source is an AUR package for which it should search
:cvar Directory: source is a directory which contains packages
:cvar Local: source is locally stored PKGBUILD
:cvar Remote: source is remote (http, ftp etc) link
:cvar Repository: source is official repository
Attributes:
Auto(PackageSource): (class attribute) automatically determine type of the source
Archive(PackageSource): (class attribute) source is a package archive
AUR(PackageSource): (class attribute) source is an AUR package for which it should search
Directory(PackageSource): (class attribute) source is a directory which contains packages
Local(PackageSource): (class attribute) source is locally stored PKGBUILD
Remote(PackageSource): (class attribute) source is remote (http, ftp etc) link
Repository(PackageSource): (class attribute) source is official repository
"""
Auto = "auto"
@ -49,8 +51,12 @@ class PackageSource(Enum):
def resolve(self, source: str) -> PackageSource:
"""
resolve auto into the correct type
:param source: source of the package
:return: non-auto type of the package source
Args:
source(str): source of the package
Returns:
PackageSource: non-auto type of the package source
"""
if self != PackageSource.Auto:
return self

View File

@ -25,9 +25,11 @@ from typing import Any
class Property:
"""
holder of object properties descriptor
:ivar name: name of the property
:ivar value: property value
:ivar is_required: if set to True then this property is required
Attributes:
name(str): name of the property
value(Any): property value
is_required(bool): if set to True then this property is required
"""
name: str

View File

@ -22,17 +22,17 @@ from __future__ import annotations
from enum import Enum
from typing import Type
from ahriman.core.exceptions import InvalidOption
class ReportSettings(Enum):
"""
report targets enumeration
:cvar Disabled: option which generates no report for testing purpose
:cvar HTML: html report generation
:cvar Email: email report generation
:cvar Console: print result to console
:cvar Telegram: markdown report to telegram channel
Attributes:
Disabled(ReportSettings): (class attribute) option which generates no report for testing purpose
HTML(ReportSettings): (class attribute) html report generation
Email(ReportSettings): (class attribute) email report generation
Console(ReportSettings): (class attribute) print result to console
Telegram(ReportSettings): (class attribute) markdown report to telegram channel
"""
Disabled = "disabled" # for testing purpose
@ -45,8 +45,12 @@ class ReportSettings(Enum):
def from_option(cls: Type[ReportSettings], value: str) -> ReportSettings:
"""
construct value from configuration
:param value: configuration value
:return: parsed value
Args:
value(str): configuration value
Returns:
ReportSettings: parsed value
"""
if value.lower() in ("html",):
return cls.HTML
@ -56,4 +60,4 @@ class ReportSettings(Enum):
return cls.Console
if value.lower() in ("telegram",):
return cls.Telegram
raise InvalidOption(value)
return cls.Disabled

View File

@ -33,8 +33,10 @@ from ahriman.core.exceptions import InvalidPath
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
Attributes:
root(Path): repository root (i.e. ahriman home)
architecture(str): repository architecture
"""
root: Path
@ -43,14 +45,20 @@ class RepositoryPaths:
@property
def cache(self) -> Path:
"""
:return: directory for packages cache (mainly used for VCS packages)
get directory for packages cache (mainly used for VCS packages)
Returns:
Path: full path to cache directory
"""
return self.root / "cache"
@property
def chroot(self) -> Path:
"""
:return: directory for devtools chroot
get directory for devtools chroot
Returns:
Path: full patch to devtools chroot directory
"""
# for the chroot directory devtools will create own tree, and we don"t have to specify architecture here
return self.root / "chroot"
@ -58,21 +66,30 @@ class RepositoryPaths:
@property
def packages(self) -> Path:
"""
:return: directory for built packages
get directory for built packages
Returns:
Path: full path to built packages directory
"""
return self.root / "packages" / self.architecture
@property
def repository(self) -> Path:
"""
:return: repository directory
get repository directory
Returns:
Path: full path to the repository directory
"""
return self.root / "repository" / self.architecture
@property
def root_owner(self) -> Tuple[int, int]:
"""
:return: owner user and group of the root directory
get UID and GID of the root directory
Returns:
Tuple[int, int]: owner user and group of the root directory
"""
return self.owner(self.root)
@ -80,8 +97,12 @@ class RepositoryPaths:
def known_architectures(cls: Type[RepositoryPaths], root: Path) -> Set[str]:
"""
get known architectures
:param root: repository root
:return: list of architectures for which tree is created
Args:
root(Path): repository root
Returns:
Set[str]: list of architectures for which tree is created
"""
paths = cls(root, "")
return {
@ -94,8 +115,12 @@ class RepositoryPaths:
def owner(path: Path) -> Tuple[int, int]:
"""
retrieve owner information by path
:param path: path for which extract ids
:return: owner user and group ids of the directory
Args:
path(Path): path for which extract ids
Returns:
Tuple[int, int]: owner user and group ids of the directory
"""
stat = path.stat()
return stat.st_uid, stat.st_gid
@ -103,21 +128,26 @@ class RepositoryPaths:
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
:param package_base: package base name
:return: full path to directory for specified package base cache
Args:
package_base(str): package base name
Returns:
Path: full path to directory for specified package base cache
"""
return self.cache / package_base
def chown(self, path: Path) -> None:
"""
set owner of path recursively (from root) to root owner
:param path: path to be chown
Args:
path(Path): path to be chown
Raises:
InvalidPath: if path does not belong to root
"""
def set_owner(current: Path) -> None:
"""
set owner to the specified path
:param current: path to set
"""
uid, gid = self.owner(current)
if uid == root_uid and gid == root_gid:
return
@ -133,7 +163,9 @@ class RepositoryPaths:
def tree_clear(self, package_base: str) -> None:
"""
clear package specific files
:param package_base: package base name
Args:
package_base(str): package base name
"""
for directory in (
self.cache_for(package_base),

View File

@ -33,8 +33,10 @@ class Result:
def __init__(self, success: Optional[Iterable[Package]] = None, failed: Optional[Iterable[Package]] = None) -> None:
"""
default constructor
:param success: initial list of successes packages
:param failed: initial list of failed packages
Args:
success(Optional[Iterable[Package]], optional): initial list of successes packages (Default value = None)
failed(Optional[Iterable[Package]], optional): initial list of failed packages (Default value = None)
"""
success = success or []
self._success = {package.base: package for package in success}
@ -44,35 +46,48 @@ class Result:
@property
def failed(self) -> List[Package]:
"""
:return: list of packages which were failed
get list of failed packages
Returns:
List[Package]: list of packages which were failed
"""
return list(self._failed.values())
@property
def is_empty(self) -> bool:
"""
:return: True in case if success list is empty and False otherwise
get if build result is empty or not
Returns:
bool: True in case if success list is empty and False otherwise
"""
return not bool(self._success)
@property
def success(self) -> List[Package]:
"""
:return: list of packages with success result
get list of success builds
Returns:
List[Package]: list of packages with success result
"""
return list(self._success.values())
def add_failed(self, package: Package) -> None:
"""
add new package to failed built
:param package: package with errors during build
Args:
package(Package): package with errors during build
"""
self._failed[package.base] = package
def add_success(self, package: Package) -> None:
"""
add new package to success built
:param package: package built
Args:
package(Package): package built
"""
self._success[package.base] = package
@ -80,8 +95,15 @@ class Result:
def merge(self, other: Result) -> Result:
"""
merge other result into this one. This method assumes that other has fresh info about status and override it
:param other: instance of the newest result
:return: updated instance
Args:
other(Result): instance of the newest result
Returns:
Result: updated instance
Raises:
SuccessFailed: if there is previously failed package which is masked as success
"""
for base, package in other._failed.items():
if base in self._success:
@ -97,8 +119,12 @@ class Result:
def __eq__(self, other: Any) -> bool:
"""
check if other is the same object
:param other: other object instance
:return: True if the other object is the same and False otherwise
Args:
other(Any): other object instance
Returns:
bool: True if the other object is the same and False otherwise
"""
if not isinstance(other, Result):
return False

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