add raises note

Also change behaviour of the `from_option` method to fallback to
disabled instead of raising exception on unknown option
This commit is contained in:
Evgenii Alekseev 2022-04-17 05:44:46 +03:00
parent 5b6ba721fe
commit e2f7e9cf28
62 changed files with 245 additions and 99 deletions

View File

@ -42,6 +42,9 @@ class Packages(Properties):
Args: Args:
result(Result): build result result(Result): build result
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError
@ -51,6 +54,9 @@ class Packages(Properties):
Returns: Returns:
Set[str]: list of known packages Set[str]: list of known packages
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError
@ -116,7 +122,7 @@ class Packages(Properties):
add package from remote sources (e.g. HTTP) add package from remote sources (e.g. HTTP)
Args: 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? dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
response = requests.get(source, stream=True) response = requests.get(source, stream=True)

View File

@ -41,6 +41,9 @@ class Repository(Properties):
Args: Args:
result(Result): build result result(Result): build result
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError
@ -187,7 +190,7 @@ class Repository(Properties):
no_manual(bool): do not check for manual updates no_manual(bool): do not check for manual updates
no_vcs(bool): do not check VCS packages no_vcs(bool): do not check VCS packages
log_fn(Callable[[str]): logger function to log updates log_fn(Callable[[str]): logger function to log updates
None]: None]:
Returns: Returns:
List[Package]: list of out-of-dated packages List[Package]: list of out-of-dated packages

View File

@ -53,6 +53,9 @@ class Handler:
Returns: Returns:
List[str]: list of architectures for which tree is created 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: 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 # for some parsers (e.g. config) we need to run with specific architecture
@ -105,6 +108,9 @@ class Handler:
Returns: Returns:
int: 0 on success, 1 otherwise int: 0 on success, 1 otherwise
Raises:
MultipleArchitectures: if more than one architecture supplied and no multi architecture supported
""" """
architectures = cls.architectures_extract(args) architectures = cls.architectures_extract(args)
@ -133,6 +139,9 @@ class Handler:
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
no_report(bool): force disable reporting no_report(bool): force disable reporting
unsafe(bool): if set no user check will be performed before path creation unsafe(bool): if set no user check will be performed before path creation
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError
@ -144,6 +153,9 @@ class Handler:
Args: Args:
enabled(bool): if False no check will be performed enabled(bool): if False no check will be performed
predicate(bool): indicates condition on which exception should be thrown predicate(bool): indicates condition on which exception should be thrown
Raises:
ExitCode: if result is empty and check is enabled
""" """
if enabled and predicate: if enabled and predicate:
raise ExitCode() raise ExitCode()

View File

@ -82,7 +82,7 @@ class Patch(Handler):
Args: Args:
application(Application): application instance application(Application): application instance
package_base(Optional[str]): package base 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) patches = application.database.patches_list(package_base)
Patch.check_if_empty(exit_code, not patches) Patch.check_if_empty(exit_code, not patches)

View File

@ -75,6 +75,9 @@ class Search(Handler):
Returns: Returns:
List[AURPackage]: sorted list for packages 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: if sort_by not in Search.SORT_FIELDS:
raise InvalidOption(sort_by) raise InvalidOption(sort_by)

View File

@ -24,7 +24,6 @@ from typing import List, Type
from ahriman.application.handlers.handler import Handler from ahriman.application.handlers.handler import Handler
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
from ahriman.core.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
@ -67,8 +66,7 @@ class UnsafeCommands(Handler):
parser(argparse.ArgumentParser): generated argument parser parser(argparse.ArgumentParser): generated argument parser
""" """
args = parser.parse_args(shlex.split(command)) args = parser.parse_args(shlex.split(command))
if args.command in unsafe_commands: UnsafeCommands.check_if_empty(True, args.command in unsafe_commands)
raise ExitCode()
@staticmethod @staticmethod
def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]: def get_unsafe_commands(parser: argparse.ArgumentParser) -> List[str]:

View File

@ -65,7 +65,7 @@ class Lock:
def __enter__(self) -> Lock: def __enter__(self) -> Lock:
""" """
default workflow is the following: default workflow is the following:
check user UID check user UID
check if there is lock file check if there is lock file
check web status watcher status check web status watcher status
@ -124,6 +124,9 @@ class Lock:
def create(self) -> None: def create(self) -> None:
""" """
create lock file create lock file
Raises:
DuplicateRun: if lock exists and no force flag supplied
""" """
if self.path is None: if self.path is None:
return return

View File

@ -63,6 +63,9 @@ class AUR(Remote):
Returns: Returns:
List[AURPackage]: list of parsed packages List[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
""" """
response_type = response["type"] response_type = response["type"]
if response_type == "error": if response_type == "error":

View File

@ -58,6 +58,9 @@ class Official(Remote):
Returns: Returns:
List[AURPackage]: list of parsed packages List[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
""" """
if not response["valid"]: if not response["valid"]:
raise InvalidPackageInfo("API validation error") raise InvalidPackageInfo("API validation error")

View File

@ -98,6 +98,9 @@ class Remote:
Returns: Returns:
AURPackage: package which match the package name AURPackage: package which match the package name
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError
@ -110,5 +113,8 @@ class Remote:
Returns: Returns:
List[AURPackage]: list of packages which match the criteria List[AURPackage]: list of packages which match the criteria
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -79,6 +79,9 @@ class OAuth(Mapping):
Returns: Returns:
Type[aioauth_client.OAuth2Client]: loaded provider type 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) provider: Type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
try: try:

View File

@ -166,7 +166,7 @@ class Sources:
Args: Args:
sources_dir(Path): local path to git repository sources_dir(Path): local path to git repository
*pattern(str): *pattern(str): glob patterns
Returns: Returns:
str: patch as plain text str: patch as plain text

View File

@ -34,7 +34,7 @@ from ahriman.models.repository_paths import RepositoryPaths
class Configuration(configparser.RawConfigParser): class Configuration(configparser.RawConfigParser):
""" """
extension for built-in configuration parser extension for built-in configuration parser
Attributes: Attributes:
ARCHITECTURE_SPECIFIC_SECTIONS(List[str]): (class attribute) known sections which can be architecture specific (required by dump) 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) DEFAULT_LOG_FORMAT(str): (class attribute) default log format (in case of fallback)
@ -115,6 +115,9 @@ class Configuration(configparser.RawConfigParser):
Returns: Returns:
List[str]: list of string from the parsed string 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]: def generator() -> Generator[str, None, None]:
quote_mark = None quote_mark = None
@ -170,6 +173,9 @@ class Configuration(configparser.RawConfigParser):
Returns: Returns:
Tuple[Path, str]: configuration root path and architecture if loaded 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: if self.path is None or self.architecture is None:
raise InitializeException("Configuration path and/or architecture are not set") raise InitializeException("Configuration path and/or architecture are not set")
@ -204,6 +210,9 @@ class Configuration(configparser.RawConfigParser):
Returns: Returns:
Tuple[str, str]: section name and found type name 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 group_type = self.get(section, "type", fallback=None) # new-style logic
if group_type is not None: if group_type is not None:

View File

@ -36,7 +36,7 @@ from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
class Email(Report, JinjaTemplate): class Email(Report, JinjaTemplate):
""" """
email report generator email report generator
Attributes: Attributes:
full_template_path(Path): path to template for full package list full_template_path(Path): path to template for full package list
host(str): SMTP host to connect host(str): SMTP host to connect

View File

@ -32,9 +32,9 @@ from ahriman.models.sign_settings import SignSettings
class JinjaTemplate: class JinjaTemplate:
""" """
jinja based report generator jinja based report generator
It uses jinja2 templates for report generation, the following variables are allowed: It uses jinja2 templates for report generation, the following variables are allowed:
homepage - link to homepage, string, optional homepage - link to homepage, string, optional
link_path - prefix fo packages to download, string, required link_path - prefix fo packages to download, string, required
has_package_signed - True in case if package sign enabled, False otherwise, required has_package_signed - True in case if package sign enabled, False otherwise, required

View File

@ -97,6 +97,9 @@ class Report:
Args: Args:
packages(Iterable[Package]): list of packages to generate report packages(Iterable[Package]): list of packages to generate report
result(Result): build result result(Result): build result
Raises:
ReportFailed: in case of any report unmatched exception
""" """
try: try:
self.generate(packages, result) self.generate(packages, result)

View File

@ -36,6 +36,9 @@ class Cleaner(Properties):
Returns: Returns:
List[Path]: list of filenames from the directory List[Path]: list of filenames from the directory
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -45,6 +45,9 @@ class Executor(Cleaner):
Returns: Returns:
List[Package]: list of read packages List[Package]: list of read packages
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError
@ -54,6 +57,9 @@ class Executor(Cleaner):
Returns: Returns:
List[Package]: list of packages properties List[Package]: list of packages properties
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -32,7 +32,7 @@ from ahriman.core.util import check_user
class Properties: class Properties:
""" """
repository internal objects holder repository internal objects holder
Attributes: Attributes:
architecture(str): repository architecture architecture(str): repository architecture
aur_url(str): base AUR url aur_url(str): base AUR url

View File

@ -82,7 +82,7 @@ class Repository(Executor, UpdateHandler):
extract list of packages which depends on specified package extract list of packages which depends on specified package
Args: Args:
depends_on(Optional[Iterable[str]]): depends_on(Optional[Iterable[str]]): dependencies of the packages
Returns: Returns:
List[Package]: list of repository packages which depend on specified packages List[Package]: list of repository packages which depend on specified packages

View File

@ -36,6 +36,9 @@ class UpdateHandler(Cleaner):
Returns: Returns:
List[Package]: list of packages properties List[Package]: list of packages properties
Raises:
NotImplementedError: not implemented method
""" """
raise NotImplementedError raise NotImplementedError

View File

@ -94,10 +94,12 @@ class GPG:
Returns: Returns:
Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key Tuple[Set[SignSettings], Optional[str]]: tuple of sign targets and default PGP key
""" """
targets = { targets: Set[SignSettings] = set()
SignSettings.from_option(option) for option in configuration.getlist("sign", "target"):
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 default_key = configuration.get("sign", "key") if targets else None
return targets, default_key return targets, default_key

View File

@ -32,7 +32,7 @@ from ahriman.models.package import Package
class Watcher: class Watcher:
""" """
package status watcher package status watcher
Attributes: Attributes:
architecture(str): repository architecture architecture(str): repository architecture
database(SQLite): database instance database(SQLite): database instance
@ -73,10 +73,13 @@ class Watcher:
get current package base build status get current package base build status
Args: Args:
base(str): base(str): package base
Returns: Returns:
Tuple[Package, BuildStatus]: package and its status Tuple[Package, BuildStatus]: package and its status
Raises:
UnknownPackage: if no package found
""" """
try: try:
return self.known[base] return self.known[base]
@ -117,6 +120,9 @@ class Watcher:
package_base(str): package base to update package_base(str): package base to update
status(BuildStatusEnum): new build status status(BuildStatusEnum): new build status
package(Optional[Package]): optional new package description. In case if not set current properties will be used 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: if package is None:
try: try:

View File

@ -102,7 +102,7 @@ class HttpUpload(Upload):
Args: Args:
method(str): request method method(str): request method
url(str): request url url(str): request url
**kwargs(Any): **kwargs(Any): request parameters to be passed as is
Returns: Returns:
requests.Response: request response object requests.Response: request response object

View File

@ -46,7 +46,7 @@ class S3(Upload):
Args: Args:
architecture(str): repository architecture architecture(str): repository architecture
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
section(str): section(str): settings section name
""" """
Upload.__init__(self, architecture, configuration) Upload.__init__(self, architecture, configuration)
self.bucket = self.get_bucket(configuration, section) self.bucket = self.get_bucket(configuration, section)

View File

@ -85,6 +85,9 @@ class Upload:
Args: Args:
path(Path): local path to sync path(Path): local path to sync
built_packages(Iterable[Package]): list of packages which has just been built built_packages(Iterable[Package]): list of packages which has just been built
Raises:
SyncFailed: in case of any synchronization unmatched exception
""" """
try: try:
self.sync(path, built_packages) self.sync(path, built_packages)

View File

@ -48,6 +48,9 @@ def check_output(*args: str, exception: Optional[Exception], cwd: Optional[Path]
Returns: Returns:
str: command output str: command output
Raises:
subprocess.CalledProcessError: if subprocess ended with status code different from 0 and no exception supplied
""" """
def log(single: str) -> None: def log(single: str) -> None:
if logger is not None: if logger is not None:
@ -91,6 +94,9 @@ def check_user(paths: RepositoryPaths, unsafe: bool) -> None:
Args: Args:
paths(RepositoryPaths): repository paths object paths(RepositoryPaths): repository paths object
unsafe(bool): if set no user check will be performed before path creation 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(): if not paths.root.exists():
return # no directory found, skip check return # no directory found, skip check
@ -187,6 +193,9 @@ def pretty_size(size: Optional[float], level: int = 0) -> str:
Returns: Returns:
str: pretty printable size as string str: pretty printable size as string
Raises:
InvalidOption: if size is more than 1TiB
""" """
def str_level() -> str: def str_level() -> str:
if level == 0: if level == 0:

View File

@ -32,7 +32,7 @@ from ahriman.core.util import filter_json, full_version
class AURPackage: class AURPackage:
""" """
AUR package descriptor AUR package descriptor
Attributes: Attributes:
id(int): package ID id(int): package ID
name(str): package name name(str): package name

View File

@ -22,8 +22,6 @@ from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Type from typing import Type
from ahriman.core.exceptions import InvalidOption
class AuthSettings(Enum): class AuthSettings(Enum):
""" """
@ -50,13 +48,11 @@ class AuthSettings(Enum):
Returns: Returns:
AuthSettings: parsed value AuthSettings: parsed value
""" """
if value.lower() in ("disabled", "no"):
return cls.Disabled
if value.lower() in ("configuration", "mapping"): if value.lower() in ("configuration", "mapping"):
return cls.Configuration return cls.Configuration
if value.lower() in ('oauth', 'oauth2'): if value.lower() in ('oauth', 'oauth2'):
return cls.OAuth return cls.OAuth
raise InvalidOption(value) return cls.Disabled
@property @property
def is_enabled(self) -> bool: def is_enabled(self) -> bool:

View File

@ -31,7 +31,7 @@ from ahriman.core.util import filter_json, pretty_datetime
class BuildStatusEnum(Enum): class BuildStatusEnum(Enum):
""" """
build status enumeration build status enumeration
Attributes: Attributes:
Unknown(BuildStatusEnum): (class attribute) build status is unknown Unknown(BuildStatusEnum): (class attribute) build status is unknown
Pending(BuildStatusEnum): (class attribute) package is out-of-dated and will be built soon Pending(BuildStatusEnum): (class attribute) package is out-of-dated and will be built soon

View File

@ -31,7 +31,7 @@ from ahriman.models.package import Package
class Counters: class Counters:
""" """
package counters package counters
Attributes: Attributes:
total(int): total packages count total(int): total packages count
unknown(int): packages in unknown status count unknown(int): packages in unknown status count

View File

@ -29,7 +29,7 @@ from ahriman.models.counters import Counters
class InternalStatus: class InternalStatus:
""" """
internal server status internal server status
Attributes: Attributes:
architecture(Optional[str]): repository architecture architecture(Optional[str]): repository architecture
packages(Counters): packages statuses counter object packages(Counters): packages statuses counter object

View File

@ -40,6 +40,9 @@ class MigrationResult:
""" """
Returns: Returns:
bool: True in case if it requires migrations and False otherwise 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() self.validate()
return self.new_version > self.old_version return self.new_version > self.old_version

View File

@ -161,6 +161,9 @@ class Package:
Returns: Returns:
Package: package properties Package: package properties
Raises:
InvalidPackageInfo: if there are parsing errors
""" """
srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text()) srcinfo, errors = parse_srcinfo((path / ".SRCINFO").read_text())
if errors: if errors:
@ -219,6 +222,9 @@ class Package:
Returns: Returns:
Package: package properties Package: package properties
Raises:
InvalidPackageInfo: if supplied package source is not valid
""" """
try: try:
resolved_source = source.resolve(package) resolved_source = source.resolve(package)
@ -246,6 +252,9 @@ class Package:
Returns: Returns:
Set[str]: list of package dependencies including makedepends array, but excluding packages from this base 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 # additional function to remove versions from dependencies
def extract_packages(raw_packages_list: List[str]) -> Set[str]: def extract_packages(raw_packages_list: List[str]) -> Set[str]:
@ -277,6 +286,9 @@ class Package:
Returns: Returns:
str: package version if package is not VCS and current version according to VCS otherwise 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: if not self.is_vcs:
return self.version return self.version

View File

@ -31,7 +31,7 @@ from ahriman.core.util import filter_json
class PackageDescription: class PackageDescription:
""" """
package specific properties package specific properties
Attributes: Attributes:
architecture(Optional[str]): package architecture architecture(Optional[str]): package architecture
archive_size(Optional[int]): package archive size archive_size(Optional[int]): package archive size

View File

@ -29,7 +29,7 @@ from ahriman.core.util import package_like
class PackageSource(Enum): class PackageSource(Enum):
""" """
package source for addition enumeration package source for addition enumeration
Attributes: Attributes:
Auto(PackageSource): (class attribute) automatically determine type of the source Auto(PackageSource): (class attribute) automatically determine type of the source
Archive(PackageSource): (class attribute) source is a package archive Archive(PackageSource): (class attribute) source is a package archive

View File

@ -22,13 +22,11 @@ from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Type from typing import Type
from ahriman.core.exceptions import InvalidOption
class ReportSettings(Enum): class ReportSettings(Enum):
""" """
report targets enumeration report targets enumeration
Attributes: Attributes:
Disabled(ReportSettings): (class attribute) option which generates no report for testing purpose Disabled(ReportSettings): (class attribute) option which generates no report for testing purpose
HTML(ReportSettings): (class attribute) html report generation HTML(ReportSettings): (class attribute) html report generation
@ -62,4 +60,4 @@ class ReportSettings(Enum):
return cls.Console return cls.Console
if value.lower() in ("telegram",): if value.lower() in ("telegram",):
return cls.Telegram return cls.Telegram
raise InvalidOption(value) return cls.Disabled

View File

@ -133,6 +133,9 @@ class RepositoryPaths:
Args: Args:
path(Path): path to be chown path(Path): path to be chown
Raises:
InvalidPath: if path does not belong to root
""" """
def set_owner(current: Path) -> None: def set_owner(current: Path) -> None:
uid, gid = self.owner(current) uid, gid = self.owner(current)

View File

@ -95,6 +95,9 @@ class Result:
Returns: Returns:
Result: updated instance Result: updated instance
Raises:
SuccessFailed: if there is previously failed package which is masked as success
""" """
for base, package in other._failed.items(): for base, package in other._failed.items():
if base in self._success: if base in self._success:

View File

@ -22,18 +22,18 @@ from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Type from typing import Type
from ahriman.core.exceptions import InvalidOption
class SignSettings(Enum): class SignSettings(Enum):
""" """
sign targets enumeration sign targets enumeration
Attributes: Attributes:
Disabled(SignSettings): (class attribute) option which generates no report for testing purpose
Packages(SignSettings): (class attribute) sign each package Packages(SignSettings): (class attribute) sign each package
Repository(SignSettings): (class attribute) sign repository database file Repository(SignSettings): (class attribute) sign repository database file
""" """
Disabled = "disabled"
Packages = "pacakges" Packages = "pacakges"
Repository = "repository" Repository = "repository"
@ -47,9 +47,12 @@ class SignSettings(Enum):
Returns: Returns:
SignSettings: parsed value SignSettings: parsed value
Raises:
InvalidOption: if unsupported option suppled
""" """
if value.lower() in ("package", "packages", "sign-package"): if value.lower() in ("package", "packages", "sign-package"):
return cls.Packages return cls.Packages
if value.lower() in ("repository", "sign-repository"): if value.lower() in ("repository", "sign-repository"):
return cls.Repository return cls.Repository
raise InvalidOption(value) return cls.Disabled

View File

@ -22,13 +22,11 @@ from __future__ import annotations
from enum import Enum from enum import Enum
from typing import Type from typing import Type
from ahriman.core.exceptions import InvalidOption
class UploadSettings(Enum): class UploadSettings(Enum):
""" """
remote synchronization targets enumeration remote synchronization targets enumeration
Attributes: Attributes:
Disabled(UploadSettings): (class attribute) no sync will be performed, required for testing purpose Disabled(UploadSettings): (class attribute) no sync will be performed, required for testing purpose
Rsync(UploadSettings): (class attribute) sync via rsync Rsync(UploadSettings): (class attribute) sync via rsync
@ -58,4 +56,4 @@ class UploadSettings(Enum):
return cls.S3 return cls.S3
if value.lower() in ("github",): if value.lower() in ("github",):
return cls.Github return cls.Github
raise InvalidOption(value) return cls.Disabled

View File

@ -23,7 +23,7 @@ from enum import Enum
class UserAccess(Enum): class UserAccess(Enum):
""" """
web user access enumeration web user access enumeration
Attributes: Attributes:
Safe(UserAccess): (class attribute) user can access the page without authorization, should not be user for user configuration 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 Read(UserAccess): (class attribute) user can read the page

View File

@ -44,8 +44,8 @@ def exception_handler(logger: Logger) -> MiddlewareType:
except HTTPServerError as e: except HTTPServerError as e:
logger.exception("server exception during performing request to %s", request.path) logger.exception("server exception during performing request to %s", request.path)
return json_response(data={"error": e.reason}, status=e.status_code) return json_response(data={"error": e.reason}, status=e.status_code)
except HTTPException: except HTTPException: # just raise 2xx and 3xx codes
raise # just raise 2xx and 3xx codes raise
except Exception as e: except Exception as e:
logger.exception("unknown exception during performing request to %s", request.path) logger.exception("unknown exception during performing request to %s", request.path)
return json_response(data={"error": str(e)}, status=500) return json_response(data={"error": str(e)}, status=500)

View File

@ -36,34 +36,34 @@ from ahriman.web.views.user.logout import LogoutView
def setup_routes(application: Application, static_path: Path) -> None: def setup_routes(application: Application, static_path: Path) -> None:
""" """
setup all defined routes setup all defined routes
Available routes are: Available routes are:
GET / get build status page GET / get build status page
GET /index.html same as above GET /index.html same as above
POST /service-api/v1/add add new packages to repository POST /service-api/v1/add add new packages to repository
POST /service-api/v1/remove remove existing package from repository POST /service-api/v1/remove remove existing package from repository
POST /service-api/v1/request request to add new packages to repository POST /service-api/v1/request request to add new packages to repository
GET /service-api/v1/search search for substring in AUR 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 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 GET /status-api/v1/ahriman get current service status
POST /status-api/v1/ahriman update service status POST /status-api/v1/ahriman update service status
GET /status-api/v1/packages get all known packages GET /status-api/v1/packages get all known packages
POST /status-api/v1/packages force update every package from repository POST /status-api/v1/packages force update every package from repository
DELETE /status-api/v1/package/:base delete package base from status page DELETE /status-api/v1/package/:base delete package base from status page
GET /status-api/v1/package/:base get package base status GET /status-api/v1/package/:base get package base status
POST /status-api/v1/package/:base update package base status POST /status-api/v1/package/:base update package base status
GET /status-api/v1/status get web service status itself GET /status-api/v1/status get web service status itself
GET /user-api/v1/login OAuth2 handler for login GET /user-api/v1/login OAuth2 handler for login
POST /user-api/v1/login login to service POST /user-api/v1/login login to service
POST /user-api/v1/logout logout from service POST /user-api/v1/logout logout from service

View File

@ -31,9 +31,9 @@ from ahriman.web.views.base import BaseView
class IndexView(BaseView): class IndexView(BaseView):
""" """
root view root view
It uses jinja2 templates for report generation, the following variables are allowed: It uses jinja2 templates for report generation, the following variables are allowed:
architecture - repository architecture, string, required architecture - repository architecture, string, required
auth - authorization descriptor, required auth - authorization descriptor, required
* authenticated - alias to check if user can see the page, boolean, required * authenticated - alias to check if user can see the page, boolean, required

View File

@ -36,11 +36,15 @@ class AddView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
add new package add new package
JSON body must be supplied, the following model is used: JSON body must be supplied, the following model is used:
{ {
"packages": "ahriman" # either list of packages or package name as in AUR "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: try:
data = await self.extract_data(["packages"]) data = await self.extract_data(["packages"])

View File

@ -36,11 +36,15 @@ class RemoveView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
remove existing packages remove existing packages
JSON body must be supplied, the following model is used: JSON body must be supplied, the following model is used:
{ {
"packages": "ahriman", # either list of packages or package name "packages": "ahriman", # either list of packages or package name
} }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPFound: in case of success response
""" """
try: try:
data = await self.extract_data(["packages"]) data = await self.extract_data(["packages"])

View File

@ -36,11 +36,15 @@ class RequestView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
request to add new package request to add new package
JSON body must be supplied, the following model is used: JSON body must be supplied, the following model is used:
{ {
"packages": "ahriman" # either list of packages or package name as in AUR "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: try:
data = await self.extract_data(["packages"]) data = await self.extract_data(["packages"])

View File

@ -40,11 +40,14 @@ class SearchView(BaseView):
async def get(self) -> Response: async def get(self) -> Response:
""" """
search packages in AUR search packages in AUR
search string (non empty) must be supplied as `for` parameter search string (non empty) must be supplied as `for` parameter
Returns: Returns:
Response: 200 with found package bases and descriptions sorted by base 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=[]) search: List[str] = self.request.query.getall("for", default=[])
packages = AUR.multisearch(*search) packages = AUR.multisearch(*search)

View File

@ -49,11 +49,15 @@ class AhrimanView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
update service status update service status
JSON body must be supplied, the following model is used: JSON body must be supplied, the following model is used:
{ {
"status": "unknown", # service status string, must be valid `BuildStatusEnum` "status": "unknown", # service status string, must be valid `BuildStatusEnum`
} }
Raises:
HTTPBadRequest: if bad data is supplied
HTTPNoContent: in case of success response
""" """
try: try:
data = await self.extract_data() data = await self.extract_data()

View File

@ -46,6 +46,9 @@ class PackageView(BaseView):
Returns: Returns:
Response: 200 with package description on success Response: 200 with package description on success
Raises:
HTTPNotFound: if no package was found
""" """
base = self.request.match_info["package"] base = self.request.match_info["package"]
@ -65,6 +68,9 @@ class PackageView(BaseView):
async def delete(self) -> None: async def delete(self) -> None:
""" """
delete package base from status page delete package base from status page
Raises:
HTTPNoContent: on success response
""" """
base = self.request.match_info["package"] base = self.request.match_info["package"]
self.service.remove(base) self.service.remove(base)
@ -74,13 +80,17 @@ class PackageView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
update package build status update package build status
JSON body must be supplied, the following model is used: JSON body must be supplied, the following model is used:
{ {
"status": "unknown", # package build status string, must be valid `BuildStatusEnum` "status": "unknown", # package build status string, must be valid `BuildStatusEnum`
"package": {} # package body (use `dataclasses.asdict` to generate one), optional. "package": {} # package body (use `dataclasses.asdict` to generate one), optional.
# Must be supplied in case if package base is unknown # 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"] base = self.request.match_info["package"]
data = await self.extract_data() data = await self.extract_data()

View File

@ -54,6 +54,9 @@ class PackagesView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
reload all packages from repository. No parameters supported here reload all packages from repository. No parameters supported here
Raises:
HTTPNoContent: on success response
""" """
self.service.load() self.service.load()

View File

@ -39,9 +39,14 @@ class LoginView(BaseView):
async def get(self) -> None: async def get(self) -> None:
""" """
OAuth2 response handler 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 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 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 from ahriman.core.auth.oauth import OAuth
@ -65,12 +70,16 @@ class LoginView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
login user to service login user to service
either JSON body or form data must be supplied the following fields are required: either JSON body or form data must be supplied the following fields are required:
{ {
"username": "username" # username to use for login "username": "username" # username to use for login
"password": "pa55w0rd" # password 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() data = await self.extract_data()
username = data.get("username") username = data.get("username")

View File

@ -37,6 +37,9 @@ class LogoutView(BaseView):
async def post(self) -> None: async def post(self) -> None:
""" """
logout user from the service. No parameters supported here logout user from the service. No parameters supported here
Raises:
HTTPFound: on success response
""" """
await check_authorized(self.request) await check_authorized(self.request)
await forget(self.request, HTTPFound("/")) await forget(self.request, HTTPFound("/"))

View File

@ -49,6 +49,9 @@ async def on_startup(application: web.Application) -> None:
Args: Args:
application(web.Application): web application instance application(web.Application): web application instance
Raises:
InitializeException: in case if matched could not be loaded
""" """
application.logger.info("server started") application.logger.info("server started")
try: try:

View File

@ -6,7 +6,6 @@ from pytest_mock import MockerFixture
from ahriman.application.ahriman import _parser from ahriman.application.ahriman import _parser
from ahriman.application.handlers import UnsafeCommands from ahriman.application.handlers import UnsafeCommands
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode
def _default_args(args: argparse.Namespace) -> argparse.Namespace: 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)) 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 must check if command is unsafe
""" """
with pytest.raises(ExitCode): check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_if_empty")
UnsafeCommands.check_unsafe("repo-clean", ["repo-clean"], _parser()) 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 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()) UnsafeCommands.check_unsafe("package-status", ["repo-clean"], _parser())
check_mock.assert_called_once_with(True, False)
def test_get_unsafe_commands() -> None: def test_get_unsafe_commands() -> None:

View File

@ -220,7 +220,7 @@ def database(configuration: Configuration) -> SQLite:
database fixture database fixture
Args: Args:
configuration(Configuration): configuration(Configuration): configuration fixture
Returns: Returns:
SQLite: database test instance SQLite: database test instance

View File

@ -4,6 +4,7 @@ import requests
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG from ahriman.core.sign.gpg import GPG
from ahriman.models.sign_settings import SignSettings 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) 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: def test_key_download(gpg: GPG, mocker: MockerFixture) -> None:
""" """
must download the key from public server must download the key from public server

View File

@ -1,15 +1,11 @@
import pytest
from ahriman.core.exceptions import InvalidOption
from ahriman.models.auth_settings import AuthSettings from ahriman.models.auth_settings import AuthSettings
def test_from_option_invalid() -> None: def test_from_option_invalid() -> None:
""" """
must raise exception on invalid option return disabled on invalid option
""" """
with pytest.raises(InvalidOption, match=".* `invalid`$"): assert AuthSettings.from_option("invalid") == AuthSettings.Disabled
AuthSettings.from_option("invalid")
def test_from_option_valid() -> None: def test_from_option_valid() -> None:

View File

@ -1,15 +1,11 @@
import pytest
from ahriman.core.exceptions import InvalidOption
from ahriman.models.report_settings import ReportSettings from ahriman.models.report_settings import ReportSettings
def test_from_option_invalid() -> None: def test_from_option_invalid() -> None:
""" """
must raise exception on invalid option must return disabled on invalid option
""" """
with pytest.raises(InvalidOption, match=".* `invalid`$"): assert ReportSettings.from_option("invalid") == ReportSettings.Disabled
ReportSettings.from_option("invalid")
def test_from_option_valid() -> None: def test_from_option_valid() -> None:

View File

@ -1,15 +1,11 @@
import pytest
from ahriman.core.exceptions import InvalidOption
from ahriman.models.sign_settings import SignSettings from ahriman.models.sign_settings import SignSettings
def test_from_option_invalid() -> None: def test_from_option_invalid() -> None:
""" """
must raise exception on invalid option must return disabled on invalid option
""" """
with pytest.raises(InvalidOption, match=".* `invalid`$"): assert SignSettings.from_option("invalid") == SignSettings.Disabled
SignSettings.from_option("invalid")
def test_from_option_valid() -> None: def test_from_option_valid() -> None:

View File

@ -1,15 +1,11 @@
import pytest
from ahriman.core.exceptions import InvalidOption
from ahriman.models.upload_settings import UploadSettings from ahriman.models.upload_settings import UploadSettings
def test_from_option_invalid() -> None: def test_from_option_invalid() -> None:
""" """
must raise exception on invalid option must return disabled on invalid option
""" """
with pytest.raises(InvalidOption, match=".* `invalid`$"): assert UploadSettings.from_option("invalid") == UploadSettings.Disabled
UploadSettings.from_option("invalid")
def test_from_option_valid() -> None: def test_from_option_valid() -> None: