From e2f7e9cf28954f6df73c4002e88f7525ac527ad2 Mon Sep 17 00:00:00 2001 From: Evgeniy Alekseev Date: Sun, 17 Apr 2022 05:44:46 +0300 Subject: [PATCH] add raises note Also change behaviour of the `from_option` method to fallback to disabled instead of raising exception on unknown option --- .../application/application/packages.py | 8 ++++++- .../application/application/repository.py | 5 +++- src/ahriman/application/handlers/handler.py | 12 ++++++++++ src/ahriman/application/handlers/patch.py | 2 +- src/ahriman/application/handlers/search.py | 3 +++ .../application/handlers/unsafe_commands.py | 4 +--- src/ahriman/application/lock.py | 5 +++- src/ahriman/core/alpm/remote/aur.py | 3 +++ src/ahriman/core/alpm/remote/official.py | 3 +++ src/ahriman/core/alpm/remote/remote.py | 6 +++++ src/ahriman/core/auth/oauth.py | 3 +++ src/ahriman/core/build_tools/sources.py | 2 +- src/ahriman/core/configuration.py | 11 ++++++++- src/ahriman/core/report/email.py | 2 +- src/ahriman/core/report/jinja_template.py | 4 ++-- src/ahriman/core/report/report.py | 3 +++ src/ahriman/core/repository/cleaner.py | 3 +++ src/ahriman/core/repository/executor.py | 6 +++++ src/ahriman/core/repository/properties.py | 2 +- src/ahriman/core/repository/repository.py | 2 +- src/ahriman/core/repository/update_handler.py | 3 +++ src/ahriman/core/sign/gpg.py | 10 ++++---- src/ahriman/core/status/watcher.py | 10 ++++++-- src/ahriman/core/upload/http_upload.py | 2 +- src/ahriman/core/upload/s3.py | 2 +- src/ahriman/core/upload/upload.py | 3 +++ src/ahriman/core/util.py | 9 +++++++ src/ahriman/models/aur_package.py | 2 +- src/ahriman/models/auth_settings.py | 6 +---- src/ahriman/models/build_status.py | 2 +- src/ahriman/models/counters.py | 2 +- src/ahriman/models/internal_status.py | 2 +- src/ahriman/models/migration_result.py | 3 +++ src/ahriman/models/package.py | 12 ++++++++++ src/ahriman/models/package_description.py | 2 +- src/ahriman/models/package_source.py | 2 +- src/ahriman/models/report_settings.py | 6 ++--- src/ahriman/models/repository_paths.py | 3 +++ src/ahriman/models/result.py | 3 +++ src/ahriman/models/sign_settings.py | 9 ++++--- src/ahriman/models/upload_settings.py | 6 ++--- src/ahriman/models/user_access.py | 2 +- .../web/middlewares/exception_handler.py | 4 ++-- src/ahriman/web/routes.py | 24 +++++++++---------- src/ahriman/web/views/index.py | 4 ++-- src/ahriman/web/views/service/add.py | 6 ++++- src/ahriman/web/views/service/remove.py | 6 ++++- src/ahriman/web/views/service/request.py | 6 ++++- src/ahriman/web/views/service/search.py | 5 +++- src/ahriman/web/views/status/ahriman.py | 6 ++++- src/ahriman/web/views/status/package.py | 12 +++++++++- src/ahriman/web/views/status/packages.py | 3 +++ src/ahriman/web/views/user/login.py | 13 ++++++++-- src/ahriman/web/views/user/logout.py | 3 +++ src/ahriman/web/web.py | 3 +++ .../handlers/test_handler_unsafe_commands.py | 12 ++++++---- tests/ahriman/conftest.py | 2 +- tests/ahriman/core/sign/test_gpg.py | 13 ++++++++++ tests/ahriman/models/test_auth_settings.py | 8 ++----- tests/ahriman/models/test_report_settings.py | 8 ++----- tests/ahriman/models/test_sign_settings.py | 8 ++----- tests/ahriman/models/test_upload_settings.py | 8 ++----- 62 files changed, 245 insertions(+), 99 deletions(-) diff --git a/src/ahriman/application/application/packages.py b/src/ahriman/application/application/packages.py index ea459d55..c2bdf64c 100644 --- a/src/ahriman/application/application/packages.py +++ b/src/ahriman/application/application/packages.py @@ -42,6 +42,9 @@ class Packages(Properties): Args: result(Result): build result + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError @@ -51,6 +54,9 @@ class Packages(Properties): Returns: Set[str]: list of known packages + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError @@ -116,7 +122,7 @@ class Packages(Properties): add package from remote sources (e.g. HTTP) Args: - source(str): + 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) diff --git a/src/ahriman/application/application/repository.py b/src/ahriman/application/application/repository.py index df8f5ec1..33b8024a 100644 --- a/src/ahriman/application/application/repository.py +++ b/src/ahriman/application/application/repository.py @@ -41,6 +41,9 @@ class Repository(Properties): Args: result(Result): build result + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError @@ -187,7 +190,7 @@ class Repository(Properties): no_manual(bool): do not check for manual updates no_vcs(bool): do not check VCS packages log_fn(Callable[[str]): logger function to log updates - None]: + None]: Returns: List[Package]: list of out-of-dated packages diff --git a/src/ahriman/application/handlers/handler.py b/src/ahriman/application/handlers/handler.py index f1bb8f1e..f99a1d31 100644 --- a/src/ahriman/application/handlers/handler.py +++ b/src/ahriman/application/handlers/handler.py @@ -53,6 +53,9 @@ class Handler: 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 @@ -105,6 +108,9 @@ class Handler: 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) @@ -133,6 +139,9 @@ class Handler: 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 @@ -144,6 +153,9 @@ class Handler: 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() diff --git a/src/ahriman/application/handlers/patch.py b/src/ahriman/application/handlers/patch.py index 949253d4..f316b446 100644 --- a/src/ahriman/application/handlers/patch.py +++ b/src/ahriman/application/handlers/patch.py @@ -82,7 +82,7 @@ class Patch(Handler): Args: application(Application): application instance package_base(Optional[str]): package base - exit_code(bool): raise ExitCode on empty search result + 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) diff --git a/src/ahriman/application/handlers/search.py b/src/ahriman/application/handlers/search.py index ce0ddc18..e2211869 100644 --- a/src/ahriman/application/handlers/search.py +++ b/src/ahriman/application/handlers/search.py @@ -75,6 +75,9 @@ class Search(Handler): 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) diff --git a/src/ahriman/application/handlers/unsafe_commands.py b/src/ahriman/application/handlers/unsafe_commands.py index 7261688f..41502e93 100644 --- a/src/ahriman/application/handlers/unsafe_commands.py +++ b/src/ahriman/application/handlers/unsafe_commands.py @@ -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 @@ -67,8 +66,7 @@ class UnsafeCommands(Handler): 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]: diff --git a/src/ahriman/application/lock.py b/src/ahriman/application/lock.py index 7a1849ad..b24df96a 100644 --- a/src/ahriman/application/lock.py +++ b/src/ahriman/application/lock.py @@ -65,7 +65,7 @@ class Lock: def __enter__(self) -> Lock: """ default workflow is the following: - + check user UID check if there is lock file check web status watcher status @@ -124,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 diff --git a/src/ahriman/core/alpm/remote/aur.py b/src/ahriman/core/alpm/remote/aur.py index 7f066ac1..7f4f60ec 100644 --- a/src/ahriman/core/alpm/remote/aur.py +++ b/src/ahriman/core/alpm/remote/aur.py @@ -63,6 +63,9 @@ class AUR(Remote): Returns: List[AURPackage]: list of parsed packages + + Raises: + InvalidPackageInfo: for error API response """ response_type = response["type"] if response_type == "error": diff --git a/src/ahriman/core/alpm/remote/official.py b/src/ahriman/core/alpm/remote/official.py index e0ae28a6..914455bf 100644 --- a/src/ahriman/core/alpm/remote/official.py +++ b/src/ahriman/core/alpm/remote/official.py @@ -58,6 +58,9 @@ class Official(Remote): Returns: List[AURPackage]: list of parsed packages + + Raises: + InvalidPackageInfo: for error API response """ if not response["valid"]: raise InvalidPackageInfo("API validation error") diff --git a/src/ahriman/core/alpm/remote/remote.py b/src/ahriman/core/alpm/remote/remote.py index 80fcaa56..480ad838 100644 --- a/src/ahriman/core/alpm/remote/remote.py +++ b/src/ahriman/core/alpm/remote/remote.py @@ -98,6 +98,9 @@ class Remote: Returns: AURPackage: package which match the package name + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError @@ -110,5 +113,8 @@ class Remote: Returns: List[AURPackage]: list of packages which match the criteria + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError diff --git a/src/ahriman/core/auth/oauth.py b/src/ahriman/core/auth/oauth.py index bb0ee92a..a1069c91 100644 --- a/src/ahriman/core/auth/oauth.py +++ b/src/ahriman/core/auth/oauth.py @@ -79,6 +79,9 @@ class OAuth(Mapping): 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: diff --git a/src/ahriman/core/build_tools/sources.py b/src/ahriman/core/build_tools/sources.py index 83b01bba..e6073eb0 100644 --- a/src/ahriman/core/build_tools/sources.py +++ b/src/ahriman/core/build_tools/sources.py @@ -166,7 +166,7 @@ class Sources: Args: sources_dir(Path): local path to git repository - *pattern(str): + *pattern(str): glob patterns Returns: str: patch as plain text diff --git a/src/ahriman/core/configuration.py b/src/ahriman/core/configuration.py index e6fb7df2..2a93d8e8 100644 --- a/src/ahriman/core/configuration.py +++ b/src/ahriman/core/configuration.py @@ -34,7 +34,7 @@ from ahriman.models.repository_paths import RepositoryPaths class Configuration(configparser.RawConfigParser): """ extension for built-in configuration parser - + Attributes: ARCHITECTURE_SPECIFIC_SECTIONS(List[str]): (class attribute) known sections which can be architecture specific (required by dump) DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback) @@ -115,6 +115,9 @@ class Configuration(configparser.RawConfigParser): 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 @@ -170,6 +173,9 @@ class Configuration(configparser.RawConfigParser): 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") @@ -204,6 +210,9 @@ class Configuration(configparser.RawConfigParser): 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: diff --git a/src/ahriman/core/report/email.py b/src/ahriman/core/report/email.py index f69ec532..55b68d8f 100644 --- a/src/ahriman/core/report/email.py +++ b/src/ahriman/core/report/email.py @@ -36,7 +36,7 @@ from ahriman.models.smtp_ssl_settings import SmtpSSLSettings class Email(Report, JinjaTemplate): """ email report generator - + Attributes: full_template_path(Path): path to template for full package list host(str): SMTP host to connect diff --git a/src/ahriman/core/report/jinja_template.py b/src/ahriman/core/report/jinja_template.py index 340f80ac..91e68b83 100644 --- a/src/ahriman/core/report/jinja_template.py +++ b/src/ahriman/core/report/jinja_template.py @@ -32,9 +32,9 @@ from ahriman.models.sign_settings import SignSettings class JinjaTemplate: """ jinja based report generator - + 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 diff --git a/src/ahriman/core/report/report.py b/src/ahriman/core/report/report.py index c7df5364..26d7f039 100644 --- a/src/ahriman/core/report/report.py +++ b/src/ahriman/core/report/report.py @@ -97,6 +97,9 @@ class Report: 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) diff --git a/src/ahriman/core/repository/cleaner.py b/src/ahriman/core/repository/cleaner.py index 046eba8b..d1fe8eca 100644 --- a/src/ahriman/core/repository/cleaner.py +++ b/src/ahriman/core/repository/cleaner.py @@ -36,6 +36,9 @@ class Cleaner(Properties): Returns: List[Path]: list of filenames from the directory + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError diff --git a/src/ahriman/core/repository/executor.py b/src/ahriman/core/repository/executor.py index 99cc59dc..0fcfee96 100644 --- a/src/ahriman/core/repository/executor.py +++ b/src/ahriman/core/repository/executor.py @@ -45,6 +45,9 @@ class Executor(Cleaner): Returns: List[Package]: list of read packages + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError @@ -54,6 +57,9 @@ class Executor(Cleaner): Returns: List[Package]: list of packages properties + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError diff --git a/src/ahriman/core/repository/properties.py b/src/ahriman/core/repository/properties.py index 63d3a4d4..33885b82 100644 --- a/src/ahriman/core/repository/properties.py +++ b/src/ahriman/core/repository/properties.py @@ -32,7 +32,7 @@ from ahriman.core.util import check_user class Properties: """ repository internal objects holder - + Attributes: architecture(str): repository architecture aur_url(str): base AUR url diff --git a/src/ahriman/core/repository/repository.py b/src/ahriman/core/repository/repository.py index d47a63d2..b0f36124 100644 --- a/src/ahriman/core/repository/repository.py +++ b/src/ahriman/core/repository/repository.py @@ -82,7 +82,7 @@ class Repository(Executor, UpdateHandler): extract list of packages which depends on specified package Args: - depends_on(Optional[Iterable[str]]): + depends_on(Optional[Iterable[str]]): dependencies of the packages Returns: List[Package]: list of repository packages which depend on specified packages diff --git a/src/ahriman/core/repository/update_handler.py b/src/ahriman/core/repository/update_handler.py index 3656f988..0bfb4a63 100644 --- a/src/ahriman/core/repository/update_handler.py +++ b/src/ahriman/core/repository/update_handler.py @@ -36,6 +36,9 @@ class UpdateHandler(Cleaner): Returns: List[Package]: list of packages properties + + Raises: + NotImplementedError: not implemented method """ raise NotImplementedError diff --git a/src/ahriman/core/sign/gpg.py b/src/ahriman/core/sign/gpg.py index abd80ed8..8a681ac0 100644 --- a/src/ahriman/core/sign/gpg.py +++ b/src/ahriman/core/sign/gpg.py @@ -94,10 +94,12 @@ class GPG: 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 diff --git a/src/ahriman/core/status/watcher.py b/src/ahriman/core/status/watcher.py index c5f2b7d2..5307de64 100644 --- a/src/ahriman/core/status/watcher.py +++ b/src/ahriman/core/status/watcher.py @@ -32,7 +32,7 @@ from ahriman.models.package import Package class Watcher: """ package status watcher - + Attributes: architecture(str): repository architecture database(SQLite): database instance @@ -73,10 +73,13 @@ class Watcher: get current package base build status Args: - base(str): + base(str): package base Returns: Tuple[Package, BuildStatus]: package and its status + + Raises: + UnknownPackage: if no package found """ try: return self.known[base] @@ -117,6 +120,9 @@ class Watcher: 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: diff --git a/src/ahriman/core/upload/http_upload.py b/src/ahriman/core/upload/http_upload.py index 5108c43f..fedfecad 100644 --- a/src/ahriman/core/upload/http_upload.py +++ b/src/ahriman/core/upload/http_upload.py @@ -102,7 +102,7 @@ class HttpUpload(Upload): Args: method(str): request method url(str): request url - **kwargs(Any): + **kwargs(Any): request parameters to be passed as is Returns: requests.Response: request response object diff --git a/src/ahriman/core/upload/s3.py b/src/ahriman/core/upload/s3.py index 2a511897..01f70dbb 100644 --- a/src/ahriman/core/upload/s3.py +++ b/src/ahriman/core/upload/s3.py @@ -46,7 +46,7 @@ class S3(Upload): Args: architecture(str): repository architecture configuration(Configuration): configuration instance - section(str): + section(str): settings section name """ Upload.__init__(self, architecture, configuration) self.bucket = self.get_bucket(configuration, section) diff --git a/src/ahriman/core/upload/upload.py b/src/ahriman/core/upload/upload.py index fd53c1da..2804d844 100644 --- a/src/ahriman/core/upload/upload.py +++ b/src/ahriman/core/upload/upload.py @@ -85,6 +85,9 @@ class Upload: 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) diff --git a/src/ahriman/core/util.py b/src/ahriman/core/util.py index 559f01e6..0c66a56f 100644 --- a/src/ahriman/core/util.py +++ b/src/ahriman/core/util.py @@ -48,6 +48,9 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path] 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: @@ -91,6 +94,9 @@ def check_user(paths: RepositoryPaths, unsafe: bool) -> None: 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 @@ -187,6 +193,9 @@ def pretty_size(size: Optional[float], level: int = 0) -> str: Returns: str: pretty printable size as string + + Raises: + InvalidOption: if size is more than 1TiB """ def str_level() -> str: if level == 0: diff --git a/src/ahriman/models/aur_package.py b/src/ahriman/models/aur_package.py index e1190921..740feb6d 100644 --- a/src/ahriman/models/aur_package.py +++ b/src/ahriman/models/aur_package.py @@ -32,7 +32,7 @@ from ahriman.core.util import filter_json, full_version class AURPackage: """ AUR package descriptor - + Attributes: id(int): package ID name(str): package name diff --git a/src/ahriman/models/auth_settings.py b/src/ahriman/models/auth_settings.py index f2304e47..c37d1873 100644 --- a/src/ahriman/models/auth_settings.py +++ b/src/ahriman/models/auth_settings.py @@ -22,8 +22,6 @@ from __future__ import annotations from enum import Enum from typing import Type -from ahriman.core.exceptions import InvalidOption - class AuthSettings(Enum): """ @@ -50,13 +48,11 @@ class AuthSettings(Enum): 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: diff --git a/src/ahriman/models/build_status.py b/src/ahriman/models/build_status.py index d404ea7b..fc7f2c1f 100644 --- a/src/ahriman/models/build_status.py +++ b/src/ahriman/models/build_status.py @@ -31,7 +31,7 @@ from ahriman.core.util import filter_json, pretty_datetime class BuildStatusEnum(Enum): """ build status enumeration - + Attributes: Unknown(BuildStatusEnum): (class attribute) build status is unknown Pending(BuildStatusEnum): (class attribute) package is out-of-dated and will be built soon diff --git a/src/ahriman/models/counters.py b/src/ahriman/models/counters.py index fad79b3f..0de4d06a 100644 --- a/src/ahriman/models/counters.py +++ b/src/ahriman/models/counters.py @@ -31,7 +31,7 @@ from ahriman.models.package import Package class Counters: """ package counters - + Attributes: total(int): total packages count unknown(int): packages in unknown status count diff --git a/src/ahriman/models/internal_status.py b/src/ahriman/models/internal_status.py index 8e106c3c..7d8d196b 100644 --- a/src/ahriman/models/internal_status.py +++ b/src/ahriman/models/internal_status.py @@ -29,7 +29,7 @@ from ahriman.models.counters import Counters class InternalStatus: """ internal server status - + Attributes: architecture(Optional[str]): repository architecture packages(Counters): packages statuses counter object diff --git a/src/ahriman/models/migration_result.py b/src/ahriman/models/migration_result.py index 7cb3cd1d..0821463e 100644 --- a/src/ahriman/models/migration_result.py +++ b/src/ahriman/models/migration_result.py @@ -40,6 +40,9 @@ class MigrationResult: """ Returns: bool: True in case if it requires migrations and False otherwise + + Raises: + MigrationError: if old version is newer than new one or negative """ self.validate() return self.new_version > self.old_version diff --git a/src/ahriman/models/package.py b/src/ahriman/models/package.py index 33ca3db3..3f4e6970 100644 --- a/src/ahriman/models/package.py +++ b/src/ahriman/models/package.py @@ -161,6 +161,9 @@ class Package: Returns: Package: package properties + + Raises: + InvalidPackageInfo: if there are parsing errors """ srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text()) if errors: @@ -219,6 +222,9 @@ class Package: Returns: Package: package properties + + Raises: + InvalidPackageInfo: if supplied package source is not valid """ try: resolved_source = source.resolve(package) @@ -246,6 +252,9 @@ class Package: 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]: @@ -277,6 +286,9 @@ class Package: 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 diff --git a/src/ahriman/models/package_description.py b/src/ahriman/models/package_description.py index bdc0f6e5..121a9449 100644 --- a/src/ahriman/models/package_description.py +++ b/src/ahriman/models/package_description.py @@ -31,7 +31,7 @@ from ahriman.core.util import filter_json class PackageDescription: """ package specific properties - + Attributes: architecture(Optional[str]): package architecture archive_size(Optional[int]): package archive size diff --git a/src/ahriman/models/package_source.py b/src/ahriman/models/package_source.py index 44371571..afd87018 100644 --- a/src/ahriman/models/package_source.py +++ b/src/ahriman/models/package_source.py @@ -29,7 +29,7 @@ from ahriman.core.util import package_like class PackageSource(Enum): """ package source for addition enumeration - + Attributes: Auto(PackageSource): (class attribute) automatically determine type of the source Archive(PackageSource): (class attribute) source is a package archive diff --git a/src/ahriman/models/report_settings.py b/src/ahriman/models/report_settings.py index 2eb68e9b..3185d044 100644 --- a/src/ahriman/models/report_settings.py +++ b/src/ahriman/models/report_settings.py @@ -22,13 +22,11 @@ from __future__ import annotations from enum import Enum from typing import Type -from ahriman.core.exceptions import InvalidOption - class ReportSettings(Enum): """ report targets enumeration - + Attributes: Disabled(ReportSettings): (class attribute) option which generates no report for testing purpose HTML(ReportSettings): (class attribute) html report generation @@ -62,4 +60,4 @@ class ReportSettings(Enum): return cls.Console if value.lower() in ("telegram",): return cls.Telegram - raise InvalidOption(value) + return cls.Disabled diff --git a/src/ahriman/models/repository_paths.py b/src/ahriman/models/repository_paths.py index 525666d5..8fe84128 100644 --- a/src/ahriman/models/repository_paths.py +++ b/src/ahriman/models/repository_paths.py @@ -133,6 +133,9 @@ class RepositoryPaths: Args: path(Path): path to be chown + + Raises: + InvalidPath: if path does not belong to root """ def set_owner(current: Path) -> None: uid, gid = self.owner(current) diff --git a/src/ahriman/models/result.py b/src/ahriman/models/result.py index dccb78c5..28287d8d 100644 --- a/src/ahriman/models/result.py +++ b/src/ahriman/models/result.py @@ -95,6 +95,9 @@ class 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: diff --git a/src/ahriman/models/sign_settings.py b/src/ahriman/models/sign_settings.py index 1d662687..085ad005 100644 --- a/src/ahriman/models/sign_settings.py +++ b/src/ahriman/models/sign_settings.py @@ -22,18 +22,18 @@ from __future__ import annotations from enum import Enum from typing import Type -from ahriman.core.exceptions import InvalidOption - class SignSettings(Enum): """ sign targets enumeration Attributes: + Disabled(SignSettings): (class attribute) option which generates no report for testing purpose Packages(SignSettings): (class attribute) sign each package Repository(SignSettings): (class attribute) sign repository database file """ + Disabled = "disabled" Packages = "pacakges" Repository = "repository" @@ -47,9 +47,12 @@ class SignSettings(Enum): Returns: SignSettings: parsed value + + Raises: + InvalidOption: if unsupported option suppled """ if value.lower() in ("package", "packages", "sign-package"): return cls.Packages if value.lower() in ("repository", "sign-repository"): return cls.Repository - raise InvalidOption(value) + return cls.Disabled diff --git a/src/ahriman/models/upload_settings.py b/src/ahriman/models/upload_settings.py index aae67533..1e981cea 100644 --- a/src/ahriman/models/upload_settings.py +++ b/src/ahriman/models/upload_settings.py @@ -22,13 +22,11 @@ from __future__ import annotations from enum import Enum from typing import Type -from ahriman.core.exceptions import InvalidOption - class UploadSettings(Enum): """ remote synchronization targets enumeration - + Attributes: Disabled(UploadSettings): (class attribute) no sync will be performed, required for testing purpose Rsync(UploadSettings): (class attribute) sync via rsync @@ -58,4 +56,4 @@ class UploadSettings(Enum): return cls.S3 if value.lower() in ("github",): return cls.Github - raise InvalidOption(value) + return cls.Disabled diff --git a/src/ahriman/models/user_access.py b/src/ahriman/models/user_access.py index f6ae2b59..59368bd8 100644 --- a/src/ahriman/models/user_access.py +++ b/src/ahriman/models/user_access.py @@ -23,7 +23,7 @@ from enum import Enum class UserAccess(Enum): """ web user access enumeration - + Attributes: Safe(UserAccess): (class attribute) user can access the page without authorization, should not be user for user configuration Read(UserAccess): (class attribute) user can read the page diff --git a/src/ahriman/web/middlewares/exception_handler.py b/src/ahriman/web/middlewares/exception_handler.py index f44bdcd7..4ca6514a 100644 --- a/src/ahriman/web/middlewares/exception_handler.py +++ b/src/ahriman/web/middlewares/exception_handler.py @@ -44,8 +44,8 @@ def exception_handler(logger: Logger) -> MiddlewareType: except HTTPServerError as e: logger.exception("server exception during performing request to %s", request.path) return json_response(data={"error": e.reason}, status=e.status_code) - except HTTPException: - raise # just raise 2xx and 3xx codes + except HTTPException: # just raise 2xx and 3xx codes + raise except Exception as e: logger.exception("unknown exception during performing request to %s", request.path) return json_response(data={"error": str(e)}, status=500) diff --git a/src/ahriman/web/routes.py b/src/ahriman/web/routes.py index 443dc21c..b1179574 100644 --- a/src/ahriman/web/routes.py +++ b/src/ahriman/web/routes.py @@ -36,34 +36,34 @@ from ahriman.web.views.user.logout import LogoutView def setup_routes(application: Application, static_path: Path) -> None: """ setup all defined routes - + Available routes are: - + GET / get build status page GET /index.html same as above - + POST /service-api/v1/add add new packages to repository - + POST /service-api/v1/remove remove existing package from repository - + POST /service-api/v1/request request to add new packages to repository - + GET /service-api/v1/search search for substring in AUR - + POST /service-api/v1/update update packages in repository, actually it is just alias for add - + GET /status-api/v1/ahriman get current service status POST /status-api/v1/ahriman update service status - + GET /status-api/v1/packages get all known packages POST /status-api/v1/packages force update every package from repository - + DELETE /status-api/v1/package/:base delete package base from status page GET /status-api/v1/package/:base get package base status POST /status-api/v1/package/:base update package base status - + GET /status-api/v1/status get web service status itself - + GET /user-api/v1/login OAuth2 handler for login POST /user-api/v1/login login to service POST /user-api/v1/logout logout from service diff --git a/src/ahriman/web/views/index.py b/src/ahriman/web/views/index.py index c464b366..45b83751 100644 --- a/src/ahriman/web/views/index.py +++ b/src/ahriman/web/views/index.py @@ -31,9 +31,9 @@ from ahriman.web.views.base import BaseView class IndexView(BaseView): """ root view - + It uses jinja2 templates for report generation, the following variables are allowed: - + architecture - repository architecture, string, required auth - authorization descriptor, required * authenticated - alias to check if user can see the page, boolean, required diff --git a/src/ahriman/web/views/service/add.py b/src/ahriman/web/views/service/add.py index b5d1bcd6..7482d01c 100644 --- a/src/ahriman/web/views/service/add.py +++ b/src/ahriman/web/views/service/add.py @@ -36,11 +36,15 @@ class AddView(BaseView): async def post(self) -> None: """ add new package - + JSON body must be supplied, the following model is used: { "packages": "ahriman" # either list of packages or package name as in AUR } + + Raises: + HTTPBadRequest: if bad data is supplied + HTTPFound: in case of success response """ try: data = await self.extract_data(["packages"]) diff --git a/src/ahriman/web/views/service/remove.py b/src/ahriman/web/views/service/remove.py index 4d4b9399..88a25aed 100644 --- a/src/ahriman/web/views/service/remove.py +++ b/src/ahriman/web/views/service/remove.py @@ -36,11 +36,15 @@ class RemoveView(BaseView): async def post(self) -> None: """ remove existing packages - + JSON body must be supplied, the following model is used: { "packages": "ahriman", # either list of packages or package name } + + Raises: + HTTPBadRequest: if bad data is supplied + HTTPFound: in case of success response """ try: data = await self.extract_data(["packages"]) diff --git a/src/ahriman/web/views/service/request.py b/src/ahriman/web/views/service/request.py index d4a69a48..f6880729 100644 --- a/src/ahriman/web/views/service/request.py +++ b/src/ahriman/web/views/service/request.py @@ -36,11 +36,15 @@ class RequestView(BaseView): async def post(self) -> None: """ request to add new package - + JSON body must be supplied, the following model is used: { "packages": "ahriman" # either list of packages or package name as in AUR } + + Raises: + HTTPBadRequest: if bad data is supplied + HTTPFound: in case of success response """ try: data = await self.extract_data(["packages"]) diff --git a/src/ahriman/web/views/service/search.py b/src/ahriman/web/views/service/search.py index c64b96be..5b8e3c45 100644 --- a/src/ahriman/web/views/service/search.py +++ b/src/ahriman/web/views/service/search.py @@ -40,11 +40,14 @@ class SearchView(BaseView): async def get(self) -> Response: """ search packages in AUR - + search string (non empty) must be supplied as `for` parameter Returns: Response: 200 with found package bases and descriptions sorted by base + + Raises: + HTTPNotFound: if no packages found """ search: List[str] = self.request.query.getall("for", default=[]) packages = AUR.multisearch(*search) diff --git a/src/ahriman/web/views/status/ahriman.py b/src/ahriman/web/views/status/ahriman.py index 56fc2287..3575cd47 100644 --- a/src/ahriman/web/views/status/ahriman.py +++ b/src/ahriman/web/views/status/ahriman.py @@ -49,11 +49,15 @@ class AhrimanView(BaseView): async def post(self) -> None: """ update service status - + JSON body must be supplied, the following model is used: { "status": "unknown", # service status string, must be valid `BuildStatusEnum` } + + Raises: + HTTPBadRequest: if bad data is supplied + HTTPNoContent: in case of success response """ try: data = await self.extract_data() diff --git a/src/ahriman/web/views/status/package.py b/src/ahriman/web/views/status/package.py index 055bebf3..4e649bf0 100644 --- a/src/ahriman/web/views/status/package.py +++ b/src/ahriman/web/views/status/package.py @@ -46,6 +46,9 @@ class PackageView(BaseView): Returns: Response: 200 with package description on success + + Raises: + HTTPNotFound: if no package was found """ base = self.request.match_info["package"] @@ -65,6 +68,9 @@ class PackageView(BaseView): async def delete(self) -> None: """ delete package base from status page + + Raises: + HTTPNoContent: on success response """ base = self.request.match_info["package"] self.service.remove(base) @@ -74,13 +80,17 @@ class PackageView(BaseView): async def post(self) -> None: """ update package build status - + JSON body must be supplied, the following model is used: { "status": "unknown", # package build status string, must be valid `BuildStatusEnum` "package": {} # package body (use `dataclasses.asdict` to generate one), optional. # Must be supplied in case if package base is unknown } + + Raises: + HTTPBadRequest: if bad data is supplied + HTTPNoContent: in case of success response """ base = self.request.match_info["package"] data = await self.extract_data() diff --git a/src/ahriman/web/views/status/packages.py b/src/ahriman/web/views/status/packages.py index 858d6ec1..89a33962 100644 --- a/src/ahriman/web/views/status/packages.py +++ b/src/ahriman/web/views/status/packages.py @@ -54,6 +54,9 @@ class PackagesView(BaseView): async def post(self) -> None: """ reload all packages from repository. No parameters supported here + + Raises: + HTTPNoContent: on success response """ self.service.load() diff --git a/src/ahriman/web/views/user/login.py b/src/ahriman/web/views/user/login.py index 4d24ff7c..80766a2e 100644 --- a/src/ahriman/web/views/user/login.py +++ b/src/ahriman/web/views/user/login.py @@ -39,9 +39,14 @@ class LoginView(BaseView): async def get(self) -> None: """ OAuth2 response handler - + In case if code provided it will do a request to get user email. In case if no code provided it will redirect to authorization url provided by OAuth client + + Raises: + HTTPFound: on success response + HTTPMethodNotAllowed: in case if method is used, but OAuth is disabled + HTTPUnauthorized: if case of authorization error """ from ahriman.core.auth.oauth import OAuth @@ -65,12 +70,16 @@ class LoginView(BaseView): async def post(self) -> None: """ login user to service - + either JSON body or form data must be supplied the following fields are required: { "username": "username" # username to use for login "password": "pa55w0rd" # password to use for login } + + Raises: + HTTPFound: on success response + HTTPUnauthorized: if case of authorization error """ data = await self.extract_data() username = data.get("username") diff --git a/src/ahriman/web/views/user/logout.py b/src/ahriman/web/views/user/logout.py index 61f41882..a8b5e10d 100644 --- a/src/ahriman/web/views/user/logout.py +++ b/src/ahriman/web/views/user/logout.py @@ -37,6 +37,9 @@ class LogoutView(BaseView): async def post(self) -> None: """ logout user from the service. No parameters supported here + + Raises: + HTTPFound: on success response """ await check_authorized(self.request) await forget(self.request, HTTPFound("/")) diff --git a/src/ahriman/web/web.py b/src/ahriman/web/web.py index a8bfc431..a5e96ca0 100644 --- a/src/ahriman/web/web.py +++ b/src/ahriman/web/web.py @@ -49,6 +49,9 @@ async def on_startup(application: web.Application) -> None: Args: application(web.Application): web application instance + + Raises: + InitializeException: in case if matched could not be loaded """ application.logger.info("server started") try: diff --git a/tests/ahriman/application/handlers/test_handler_unsafe_commands.py b/tests/ahriman/application/handlers/test_handler_unsafe_commands.py index fa30236e..b7ee7d24 100644 --- a/tests/ahriman/application/handlers/test_handler_unsafe_commands.py +++ b/tests/ahriman/application/handlers/test_handler_unsafe_commands.py @@ -6,7 +6,6 @@ from pytest_mock import MockerFixture from ahriman.application.ahriman import _parser from ahriman.application.handlers import UnsafeCommands from ahriman.core.configuration import Configuration -from ahriman.core.exceptions import ExitCode def _default_args(args: argparse.Namespace) -> argparse.Namespace: @@ -53,19 +52,22 @@ def test_run_check(args: argparse.Namespace, configuration: Configuration, mocke check_mock.assert_called_once_with("clean", ["command"], pytest.helpers.anyvar(int)) -def test_check_unsafe() -> None: +def test_check_unsafe(mocker: MockerFixture) -> None: """ must check if command is unsafe """ - with pytest.raises(ExitCode): - UnsafeCommands.check_unsafe("repo-clean", ["repo-clean"], _parser()) + check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty") + UnsafeCommands.check_unsafe("repo-clean", ["repo-clean"], _parser()) + check_mock.assert_called_once_with(True, True) -def test_check_unsafe_safe() -> None: +def test_check_unsafe_safe(mocker: MockerFixture) -> None: """ must check if command is safe """ + check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty") UnsafeCommands.check_unsafe("package-status", ["repo-clean"], _parser()) + check_mock.assert_called_once_with(True, False) def test_get_unsafe_commands() -> None: diff --git a/tests/ahriman/conftest.py b/tests/ahriman/conftest.py index 0155b013..ad701c51 100644 --- a/tests/ahriman/conftest.py +++ b/tests/ahriman/conftest.py @@ -220,7 +220,7 @@ def database(configuration: Configuration) -> SQLite: database fixture Args: - configuration(Configuration): + configuration(Configuration): configuration fixture Returns: SQLite: database test instance diff --git a/tests/ahriman/core/sign/test_gpg.py b/tests/ahriman/core/sign/test_gpg.py index 41a83440..33e9399f 100644 --- a/tests/ahriman/core/sign/test_gpg.py +++ b/tests/ahriman/core/sign/test_gpg.py @@ -4,6 +4,7 @@ import requests from pathlib import Path from pytest_mock import MockerFixture +from ahriman.core.configuration import Configuration from ahriman.core.sign.gpg import GPG from ahriman.models.sign_settings import SignSettings @@ -63,6 +64,18 @@ def test_sign_command(gpg_with_key: GPG) -> None: assert gpg_with_key.sign_command(Path("a"), gpg_with_key.default_key) +def test_sign_options(configuration: Configuration) -> None: + """ + must correctly parse sign options + """ + configuration.set_option("sign", "target", "repository disabled") + configuration.set_option("sign", "key", "default-key") + + target, default_key = GPG.sign_options(configuration) + assert target == {SignSettings.Repository} + assert default_key == "default-key" + + def test_key_download(gpg: GPG, mocker: MockerFixture) -> None: """ must download the key from public server diff --git a/tests/ahriman/models/test_auth_settings.py b/tests/ahriman/models/test_auth_settings.py index b3816cd3..9d46713c 100644 --- a/tests/ahriman/models/test_auth_settings.py +++ b/tests/ahriman/models/test_auth_settings.py @@ -1,15 +1,11 @@ -import pytest - -from ahriman.core.exceptions import InvalidOption from ahriman.models.auth_settings import AuthSettings def test_from_option_invalid() -> None: """ - must raise exception on invalid option + return disabled on invalid option """ - with pytest.raises(InvalidOption, match=".* `invalid`$"): - AuthSettings.from_option("invalid") + assert AuthSettings.from_option("invalid") == AuthSettings.Disabled def test_from_option_valid() -> None: diff --git a/tests/ahriman/models/test_report_settings.py b/tests/ahriman/models/test_report_settings.py index 3290866c..1b14caea 100644 --- a/tests/ahriman/models/test_report_settings.py +++ b/tests/ahriman/models/test_report_settings.py @@ -1,15 +1,11 @@ -import pytest - -from ahriman.core.exceptions import InvalidOption from ahriman.models.report_settings import ReportSettings def test_from_option_invalid() -> None: """ - must raise exception on invalid option + must return disabled on invalid option """ - with pytest.raises(InvalidOption, match=".* `invalid`$"): - ReportSettings.from_option("invalid") + assert ReportSettings.from_option("invalid") == ReportSettings.Disabled def test_from_option_valid() -> None: diff --git a/tests/ahriman/models/test_sign_settings.py b/tests/ahriman/models/test_sign_settings.py index e282c93b..a4a5dfe8 100644 --- a/tests/ahriman/models/test_sign_settings.py +++ b/tests/ahriman/models/test_sign_settings.py @@ -1,15 +1,11 @@ -import pytest - -from ahriman.core.exceptions import InvalidOption from ahriman.models.sign_settings import SignSettings def test_from_option_invalid() -> None: """ - must raise exception on invalid option + must return disabled on invalid option """ - with pytest.raises(InvalidOption, match=".* `invalid`$"): - SignSettings.from_option("invalid") + assert SignSettings.from_option("invalid") == SignSettings.Disabled def test_from_option_valid() -> None: diff --git a/tests/ahriman/models/test_upload_settings.py b/tests/ahriman/models/test_upload_settings.py index bc50e480..d7748edc 100644 --- a/tests/ahriman/models/test_upload_settings.py +++ b/tests/ahriman/models/test_upload_settings.py @@ -1,15 +1,11 @@ -import pytest - -from ahriman.core.exceptions import InvalidOption from ahriman.models.upload_settings import UploadSettings def test_from_option_invalid() -> None: """ - must raise exception on invalid option + must return disabled on invalid option """ - with pytest.raises(InvalidOption, match=".* `invalid`$"): - UploadSettings.from_option("invalid") + assert UploadSettings.from_option("invalid") == UploadSettings.Disabled def test_from_option_valid() -> None: