Compare commits

...

9 Commits

45 changed files with 5340 additions and 5194 deletions

View File

@ -122,7 +122,7 @@ Again, the most checks can be performed by `make check` command, though some add
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
```
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses) and `__new__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
@ -225,3 +225,25 @@ Again, the most checks can be performed by `make check` command, though some add
### Other checks
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
## Developers how to
### Run automated checks
```shell
make check tests
```
### Generate documentation templates
```shell
make specification
```
### Create release
```shell
make VERSION=x.y.z check tests release
```
The command above will also run checks first and will generate documentation, tags, etc., and will push them to GitHub. Other things will be handled by GitHub workflows automatically.

View File

@ -1,4 +1,4 @@
.PHONY: archive archlinux check clean directory html push specification tests version
.PHONY: archive archlinux check clean directory html release specification tests version
.DEFAULT_GOAL := archlinux
PROJECT := ahriman
@ -37,7 +37,7 @@ html: specification
rm -rf docs/html
tox -e docs-html
push: specification archlinux
release: specification archlinux
git add package/archlinux/PKGBUILD src/ahriman/__init__.py docs/ahriman-architecture.svg package/share/man/man1/ahriman.1 package/share/bash-completion/completions/_ahriman package/share/zsh/site-functions/_ahriman
git commit -m "Release $(VERSION)"
git tag "$(VERSION)"

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 993 KiB

View File

@ -43,7 +43,6 @@ Base configuration settings.
* ``database`` - path to SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
``alpm:*`` groups
-----------------
@ -86,7 +85,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of mention.
* ``triggers_known`` - optional list of ``ahriman.core.triggers.Trigger`` class implementations which are not run automatically and used only for trigger discovery and configuration validation.
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, int, optional, default ``604800``.
* ``vcs_allowed_age`` - maximal age in seconds of the VCS packages before their version will be updated with its remote source, integer, optional, default ``604800``.
``repository`` group
--------------------
@ -103,6 +102,17 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file).
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
``status`` group
----------------
Reporting to web service related settings. In most cases there is fallback to web section settings.
* ``enabled`` - enable reporting to web service, boolean, optional, default ``yes`` for backward compatibility.
* ``address`` - remote web service address with protocol, string, optional. In case of websocket, the ``http+unix`` scheme and url encoded address (e.g. ``%2Fvar%2Flib%2Fahriman`` for ``/var/lib/ahriman``) must be used, e.g. ``http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket``. In case if none set, it will be guessed from ``web`` section.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
``web`` group
-------------
@ -116,15 +126,13 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
* ``host`` - host to bind, string, optional.
* ``index_url`` - full url of the repository index page, string, optional.
* ``max_body_size`` - max body size in bytes to be validated for archive upload, integer, optional. If not set, validation will be disabled.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``port`` - port to bind, int, optional.
* ``port`` - port to bind, integer, optional.
* ``static_path`` - path to directory with static files, string, required.
* ``templates`` - path to templates directories, space separated list of strings, required.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, int, optional.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
``keyring`` group
--------------------
@ -237,7 +245,7 @@ Section name must be either ``email`` (plus optional architecture name, e.g. ``e
* ``link_path`` - prefix for HTML links, string, required.
* ``no_empty_report`` - skip report generation for empty packages list, boolean, optional, default ``yes``.
* ``password`` - SMTP password to authenticate, string, optional.
* ``port`` - SMTP port for sending emails, int, required.
* ``port`` - SMTP port for sending emails, integer, required.
* ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
* ``sender`` - SMTP sender address, string, required.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``.
@ -267,7 +275,7 @@ Section name must be either ``remote-call`` (plus optional architecture name, e.
* ``aur`` - check for AUR packages updates, boolean, optional, default ``no``.
* ``local`` - check for local packages updates, boolean, optional, default ``no``.
* ``manual`` - update manually built packages, boolean, optional, default ``no``.
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, int, optional, default ``-1``.
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, integer, optional, default ``-1``.
``telegram`` type
^^^^^^^^^^^^^^^^^
@ -282,7 +290,7 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``template`` - Jinja2 template name, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``.
* ``templates`` - path to templates directories, space separated list of strings, required.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``upload`` group
----------------
@ -312,7 +320,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
#. Generate new token. Required scope is ``public_repo`` (or ``repo`` for private repository support).
* ``repository`` - GitHub repository name, string, required. Repository must be created before any action and must have active branch (e.g. with readme).
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``use_full_release_name`` - if set to ``yes``, the release will contain both repository name and architecture, and only architecture otherwise, boolean, optional, default ``no`` (legacy behavior).
* ``username`` - GitHub authorization user, string, required. Basically the same as ``owner``.
@ -322,7 +330,7 @@ This feature requires GitHub key creation (see below). Section name must be eith
Section name must be either ``remote-service`` (plus optional architecture name, e.g. ``remote-service:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``rsync`` type
^^^^^^^^^^^^^^
@ -341,7 +349,7 @@ Requires ``boto3`` library to be installed. Section name must be either ``s3`` (
* ``type`` - type of the upload, string, optional, must be set to ``s3`` if exists.
* ``access_key`` - AWS access key ID, string, required.
* ``bucket`` - bucket name (e.g. ``bucket``), string, required.
* ``chunk_size`` - chunk size for calculating entity tags, int, optional, default 8 * 1024 * 1024.
* ``chunk_size`` - chunk size for calculating entity tags, integer, optional, default 8 * 1024 * 1024.
* ``object_path`` - path prefix for stored objects, string, optional. If none set, the prefix as in repository tree will be used.
* ``region`` - bucket region (e.g. ``eu-central-1``), string, required.
* ``secret_key`` - AWS secret access key, string, required.

View File

@ -869,12 +869,12 @@ Worker nodes configuration
.. code-block:: ini
[web]
address = master.example.com
[status]
address = https://master.example.com
username = worker-user
password = very-secure-password
As it has been mentioned above, ``web.address`` must be available for workers. In case if unix socket is used, it can be passed as ``web.unix_socket`` variable as usual. Optional ``web.username``/``web.password`` can be supplied in case if authentication was enabled on master node.
As it has been mentioned above, ``status.address`` must be available for workers. In case if unix socket is used, it can be passed in the same option as usual. Optional ``status.username``/``status.password`` can be supplied in case if authentication was enabled on master node.
#.
Each worker must call master node on success:
@ -958,7 +958,7 @@ The user ``worker-user`` has been created additionally. Worker node config (``wo
.. code-block:: ini
[web]
[status]
address = http://172.17.0.1:8080
username = worker-user
password = very-secure-password
@ -1142,7 +1142,7 @@ How to enable basic authorization
.. code-block:: ini
[web]
[status]
username = api
password = pa55w0rd

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.12.0
pkgver=2.12.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')

View File

@ -3,7 +3,6 @@ include = ahriman.ini.d
logging = ahriman.ini.d/logging.ini
apply_migrations = yes
database = /var/lib/ahriman/ahriman.db
suppress_http_log_errors = yes
[alpm]
database = /var/lib/pacman
@ -62,6 +61,10 @@ ssl = disabled
template = repo-index.jinja2
templates = /usr/share/ahriman/templates
[status]
enabled = yes
suppress_http_log_errors = yes
[telegram]
template = telegram-index.jinja2
templates = /usr/share/ahriman/templates

View File

@ -182,7 +182,12 @@
const description = response.find(Boolean);
const packages = Object.keys(description.package.packages);
const aurUrl = description.package.remote.web_url;
const upstreamUrls = Object.values(description.package.packages).map(single => single.url);
const upstreamUrls = Array.from(
new Set(
Object.values(description.package.packages)
.map(single => single.url)
)
).sort();
packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`);

View File

@ -2,7 +2,7 @@
_shtab_ahriman_subparsers=('aur-search' 'search' 'help-commands-unsafe' 'help' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-repositories' 'service-run' 'run' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '--wait-timeout' '-V' '--version')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '-V' '--version' '--wait-timeout')
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')

View File

@ -1,9 +1,9 @@
.TH AHRIMAN "1" "2023\-11\-06" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2023\-11\-13" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help-commands-unsafe,help,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-repositories,service-run,run,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [-V] [--wait-timeout WAIT_TIMEOUT] {aur-search,search,help-commands-unsafe,help,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-repositories,service-run,run,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
@ -44,15 +44,15 @@ filter by target repository
\fB\-\-unsafe\fR
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.TP
\fB\-\-wait\-timeout\fR \fI\,WAIT_TIMEOUT\/\fR
wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of
zero value, the application will wait infinitely
.TP
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.SH
COMMAND
.TP
@ -208,22 +208,22 @@ sort field by this field. In case if two packages have the same value of the spe
by name
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
usage: ahriman help\-commands\-unsafe [\-h] [command ...]
usage: ahriman help\-commands\-unsafe [\-h] [subcommand ...]
list unsafe commands as defined in default args
.TP
\fBcommand\fR
\fBsubcommand\fR
instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise
.SH COMMAND \fI\,'ahriman help'\/\fR
usage: ahriman help [\-h] [command]
usage: ahriman help [\-h] [subcommand]
show help message for application or command and exit
.TP
\fBcommand\fR
\fBsubcommand\fR
show help message for specific command
.SH COMMAND \fI\,'ahriman help\-updates'\/\fR

View File

@ -90,8 +90,8 @@ _shtab_ahriman_options=(
{--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
{-r,--repository}"[filter by target repository (default\: None)]:repository:"
"--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable (default\: False)]"
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
"(- : *)"{-V,--version}"[show program\'s version number and exit]"
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
)
_shtab_ahriman_add_options=(

View File

@ -31,6 +31,7 @@ class MethodTypeOrder(StrEnum):
Attributes:
Class(MethodTypeOrder): (class attribute) class method
Delete(MethodTypeOrder): (class attribute) destructor-like methods
Init(MethodTypeOrder): (class attribute) initialization method
Magic(MethodTypeOrder): (class attribute) other magical methods
New(MethodTypeOrder): (class attribute) constructor method
@ -40,6 +41,7 @@ class MethodTypeOrder(StrEnum):
"""
Class = "classmethod"
Delete = "del"
Init = "init"
Magic = "magic"
New = "new"
@ -76,8 +78,9 @@ class DefinitionOrder(BaseRawFileChecker):
"method-type-order",
{
"default": [
"new",
"init",
"new",
"del",
"property",
"classmethod",
"staticmethod",
@ -122,10 +125,12 @@ class DefinitionOrder(BaseRawFileChecker):
MethodTypeOrder: resolved function type
"""
# init methods
if function.name in ("__new__",):
return MethodTypeOrder.New
if function.name in ("__init__", "__post_init__"):
return MethodTypeOrder.Init
if function.name in ("__new__",):
return MethodTypeOrder.New
if function.name in ("__del__",):
return MethodTypeOrder.Delete
# decorated methods
decorators = []

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
__version__ = "2.12.0"
__version__ = "2.12.2"

View File

@ -87,11 +87,11 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("--repository-id", help=argparse.SUPPRESS)
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true")
parser.add_argument("-V", "--version", action="version", version=__version__)
parser.add_argument("--wait-timeout", help="wait for lock to be free. Negative value will lead to "
"immediate application run even if there is lock file. "
"In case of zero value, the application will wait infinitely",
type=int, default=-1)
parser.add_argument("-V", "--version", action="version", version=__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command")
@ -178,8 +178,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
"""
parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.add_argument("subcommand", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True, parser=_parser)
return parser
@ -198,7 +198,7 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit",
formatter_class=_formatter)
parser.add_argument("command", help="show help message for specific command", nargs="?")
parser.add_argument("subcommand", help="show help message for specific command", nargs="?")
parser.set_defaults(handler=handlers.Help, architecture="", lock=None, quiet=True, report=False, repository="",
unsafe=True, parser=_parser)
return parser

View File

@ -167,12 +167,16 @@ class ApplicationPackages(ApplicationProperties):
"""
raise NotImplementedError
def remove(self, names: Iterable[str]) -> None:
def remove(self, names: Iterable[str]) -> Result:
"""
remove packages from repository
Args:
names(Iterable[str]): list of packages (either base or name) to remove
Returns:
Result: removal result
"""
self.repository.process_remove(names)
self.on_result(Result())
result = self.repository.process_remove(names)
self.on_result(result)
return result

View File

@ -44,7 +44,7 @@ class Help(Handler):
report(bool): force enable or disable reporting
"""
parser: argparse.ArgumentParser = args.parser()
if args.command is None:
if args.subcommand is None:
parser.parse_args(["--help"])
else:
parser.parse_args([args.command, "--help"])
parser.parse_args([args.subcommand, "--help"])

View File

@ -24,6 +24,7 @@ from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.repository_id import RepositoryId
@ -55,7 +56,7 @@ class Rebuild(Handler):
application.print_updates(updates, log_fn=print)
return
result = application.update(updates, args.username, bump_pkgrel=args.increment)
result = application.update(updates, Packagers(args.username), bump_pkgrel=args.increment)
Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -21,6 +21,7 @@ import argparse
from pathlib import Path
from pwd import getpwuid
from urllib.parse import quote_plus as urlencode
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
@ -128,8 +129,12 @@ class Setup(Handler):
if args.web_port is not None:
configuration.set_option("web", "port", str(args.web_port))
if (host := root.get("web", "host", fallback=None)) is not None:
configuration.set_option("status", "address", f"http://{host}:{args.web_port}")
if args.web_unix_socket is not None:
configuration.set_option("web", "unix_socket", str(args.web_unix_socket))
unix_socket = str(args.web_unix_socket)
configuration.set_option("web", "unix_socket", unix_socket)
configuration.set_option("status", "address", f"http+unix://{urlencode(unix_socket)}")
if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20))

View File

@ -46,8 +46,8 @@ class UnsafeCommands(Handler):
"""
parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser)
if args.subcommand:
UnsafeCommands.check_unsafe(args.subcommand, unsafe_commands, parser)
else:
for command in unsafe_commands:
StringPrinter(command)(verbose=True)

View File

@ -249,6 +249,32 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
},
},
},
"status": {
"type": "dict",
"schema": {
"enabled": {
"type": "boolean",
"coerce": "boolean",
},
"address": {
"type": "string",
"empty": False,
"is_url": [],
},
"password": {
"type": "string",
"empty": False,
},
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",
},
"username": {
"type": "string",
"empty": False,
},
},
},
"web": {
"type": "dict",
"schema": {

View File

@ -316,21 +316,6 @@ class UnknownPackageError(ValueError):
ValueError.__init__(self, f"Package base {package_base} is unknown")
class UnprocessedPackageStatusError(ValueError):
"""
exception for merging invalid statues
"""
def __init__(self, package_base: str) -> None:
"""
default constructor
Args:
package_base(str): package base name
"""
ValueError.__init__(self, f"Package base {package_base} had status failed, but new status is success")
class UnsafeRunError(RuntimeError):
"""
exception which will be raised in case if user is not owner of repository

View File

@ -72,7 +72,9 @@ class HttpLogHandler(logging.Handler):
if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
"settings", "suppress_http_log_errors",
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
root.addHandler(handler)

View File

@ -120,6 +120,6 @@ class Email(Report, JinjaTemplate):
text = self.make_html(result, self.template)
attachments = {}
if self.template_full is not None:
attachments["index.html"] = self.make_html(Result(success=packages), self.template_full)
attachments["index.html"] = self.make_html(Result(updated=packages), self.template_full)
self._send(text, attachments)

View File

@ -58,5 +58,5 @@ class HTML(Report, JinjaTemplate):
packages(list[Package]): list of packages to generate report
result(Result): build result
"""
html = self.make_html(Result(success=packages), self.template)
html = self.make_html(Result(updated=packages), self.template)
self.report_path.write_text(html, encoding="utf8")

View File

@ -98,7 +98,7 @@ class Executor(Cleaner):
try:
packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id)
result.add_success(single)
result.add_updated(single)
except Exception:
self.reporter.set_failed(single.base)
result.add_failed(single)
@ -106,7 +106,7 @@ class Executor(Cleaner):
return result
def process_remove(self, packages: Iterable[str]) -> Path:
def process_remove(self, packages: Iterable[str]) -> Result:
"""
remove packages from list
@ -114,7 +114,7 @@ class Executor(Cleaner):
packages(Iterable[str]): list of package names or bases to remove
Returns:
Path: path to repository database
Result: remove result
"""
def remove_base(package_base: str) -> None:
try:
@ -126,9 +126,9 @@ class Executor(Cleaner):
except Exception:
self.logger.exception("could not remove base %s", package_base)
def remove_package(package: str, fn: Path) -> None:
def remove_package(package: str, archive_path: Path) -> None:
try:
self.repo.remove(package, fn) # remove the package itself
self.repo.remove(package, archive_path) # remove the package itself
except Exception:
self.logger.exception("could not remove %s", package)
@ -136,6 +136,7 @@ class Executor(Cleaner):
bases_to_remove: list[str] = []
# build package list based on user input
result = Result()
requested = set(packages)
for local in self.packages():
if local.base in packages or all(package in requested for package in local.packages):
@ -145,6 +146,7 @@ class Executor(Cleaner):
if properties.filepath is not None
})
bases_to_remove.append(local.base)
result.add_removed(local)
elif requested.intersection(local.packages.keys()):
packages_to_remove.update({
package: properties.filepath
@ -167,7 +169,7 @@ class Executor(Cleaner):
for package in bases_to_remove:
remove_base(package)
return self.repo.repo_path
return result
def process_update(self, packages: Iterable[Path], packagers: Packagers | None = None) -> Result:
"""
@ -219,7 +221,7 @@ class Executor(Cleaner):
rename(description, local.base)
update_single(description.filename, local.base, packager.key)
self.reporter.set_success(local)
result.add_success(local)
result.add_updated(local)
current_package_archives: set[str] = set()
if local.base in current_packages:

View File

@ -49,16 +49,19 @@ class Client:
"""
if not report:
return Client()
if not configuration.getboolean("status", "enabled", fallback=True): # global switch
return Client()
address = configuration.get("web", "address", fallback=None)
# new-style section
address = configuration.get("status", "address", fallback=None)
# old-style section
legacy_address = configuration.get("web", "address", fallback=None)
host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None)
socket = configuration.get("web", "unix_socket", fallback=None)
# basically we just check if there is something we can use for interaction with remote server
# at the moment (end of 2022) I think it would be much better idea to introduce flag like `enabled`,
# but it will totally break used experience
if address or (host and port) or socket:
if address or legacy_address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient
return WebClient(repository_id, configuration)
return Client()

View File

@ -22,7 +22,7 @@ import logging
import requests
from functools import cached_property
from urllib.parse import quote_plus as urlencode
from urllib.parse import quote_plus as urlencode, urlparse
from ahriman import __version__
from ahriman.core.configuration import Configuration
@ -42,7 +42,6 @@ class WebClient(Client, SyncHttpClient):
Attributes:
address(str): address of the web service
repository_id(RepositoryId): repository unique identifier
use_unix_socket(bool): use websocket or not
"""
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
@ -53,11 +52,13 @@ class WebClient(Client, SyncHttpClient):
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
"""
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
SyncHttpClient.__init__(self, configuration, "web", suppress_errors=suppress_errors)
section, self.address = self.parse_address(configuration)
suppress_errors = configuration.getboolean( # read old-style first and then fallback to new style
"settings", "suppress_http_log_errors",
fallback=configuration.getboolean("status", "suppress_http_log_errors", fallback=False))
SyncHttpClient.__init__(self, configuration, section, suppress_errors=suppress_errors)
self.repository_id = repository_id
self.address, self.use_unix_socket = self.parse_address(configuration)
@cached_property
def session(self) -> requests.Session:
@ -67,41 +68,7 @@ class WebClient(Client, SyncHttpClient):
Returns:
request.Session: created session object
"""
return self._create_session(use_unix_socket=self.use_unix_socket)
@staticmethod
def parse_address(configuration: Configuration) -> tuple[str, bool]:
"""
parse address from configuration
Args:
configuration(Configuration): configuration instance
Returns:
tuple[str, bool]: tuple of server address and socket flag (True in case if unix socket must be used)
"""
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
# special pseudo-protocol which is used for unix sockets
return f"http+unix://{urlencode(unix_socket)}", True
address = configuration.get("web", "address", fallback=None)
if not address:
# build address from host and port directly
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
address = f"http://{host}:{port}"
return address, False
def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
"""
generate new request session
Args:
use_unix_socket(bool): if set to True then unix socket session will be generated instead of native requests
Returns:
requests.Session: generated session object
"""
if use_unix_socket:
if urlparse(self.address).scheme == "http+unix":
import requests_unixsocket # type: ignore[import-untyped]
session: requests.Session = requests_unixsocket.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
@ -113,6 +80,33 @@ class WebClient(Client, SyncHttpClient):
return session
@staticmethod
def parse_address(configuration: Configuration) -> tuple[str, str]:
"""
parse address from legacy configuration
Args:
configuration(Configuration): configuration instance
Returns:
tuple[str, str]: tuple of section name and server address
"""
# new-style section
if (address := configuration.get("status", "address", fallback=None)) is not None:
return "status", address
# legacy-style section
if (unix_socket := configuration.get("web", "unix_socket", fallback=None)) is not None:
# special pseudo-protocol which is used for unix sockets
return "web", f"http+unix://{urlencode(unix_socket)}"
address = configuration.get("web", "address", fallback=None)
if not address:
# build address from host and port directly
host = configuration.get("web", "host")
port = configuration.getint("web", "port")
address = f"http://{host}:{port}"
return "web", address
def _login(self, session: requests.Session) -> None:
"""
process login to the service

View File

@ -65,6 +65,14 @@ class TriggerLoader(LazyLogging):
self._on_stop_requested = False
self.triggers: list[Trigger] = []
def __del__(self) -> None:
"""
custom destructor object which calls on_stop in case if it was requested
"""
if not self._on_stop_requested:
return
self.on_stop()
@classmethod
def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self:
"""
@ -257,11 +265,3 @@ class TriggerLoader(LazyLogging):
for trigger in self.triggers:
with self.__execute_trigger(trigger):
trigger.on_stop()
def __del__(self) -> None:
"""
custom destructor object which calls on_stop in case if it was requested
"""
if not self._on_stop_requested:
return
self.on_stop()

View File

@ -19,28 +19,50 @@
#
from __future__ import annotations
from collections.abc import Iterable
from typing import Any
from collections.abc import Iterable, Callable
from typing import Any, Self
from ahriman.core.exceptions import UnprocessedPackageStatusError
from ahriman.models.package import Package
class Result:
"""
build result class holder
Attributes:
STATUS_PRIORITIES(list[str]): (class attribute) list of statues according to their priorities
"""
def __init__(self, success: Iterable[Package] | None = None, failed: Iterable[Package] | None = None) -> None:
STATUS_PRIORITIES = [
"failed",
"removed",
"updated",
"added",
]
def __init__(self, *, added: Iterable[Package] | None = None, updated: Iterable[Package] | None = None,
removed: Iterable[Package] | None = None, failed: Iterable[Package] | None = None) -> None:
"""
default constructor
Args:
success(Iterable[Package] | None, optional): initial list of successes packages (Default value = None)
addded(Iterable[Package] | None, optional): initial list of successfully added packages
(Default value = None)
updated(Iterable[Package] | None, optional): initial list of successfully updated packages
(Default value = None)
removed(Iterable[Package] | None, optional): initial list of successfully removed packages
(Default value = None)
failed(Iterable[Package] | None, optional): initial list of failed packages (Default value = None)
"""
success = success or []
self._success = {package.base: package for package in success}
added = added or []
self._added = {package.base: package for package in added}
updated = updated or []
self._updated = {package.base: package for package in updated}
removed = removed or []
self._removed = {package.base: package for package in removed}
failed = failed or []
self._failed = {package.base: package for package in failed}
@ -62,7 +84,17 @@ class Result:
Returns:
bool: True in case if success list is empty and False otherwise
"""
return not bool(self._success)
return not self._added and not self._updated
@property
def removed(self) -> list[Package]:
"""
get list of removed packages
Returns:
list[Package]: list of packages successfully removed
"""
return list(self._removed.values())
@property
def success(self) -> list[Package]:
@ -72,7 +104,16 @@ class Result:
Returns:
list[Package]: list of packages with success result
"""
return list(self._success.values())
return list(self._added.values()) + list(self._updated.values())
def add_added(self, package: Package) -> None:
"""
add new package to new packages list
Args:
package(Package): package removed
"""
self._added[package.base] = package
def add_failed(self, package: Package) -> None:
"""
@ -83,17 +124,26 @@ class Result:
"""
self._failed[package.base] = package
def add_success(self, package: Package) -> None:
def add_removed(self, package: Package) -> None:
"""
add new package to removed list
Args:
package(Package): package removed
"""
self._removed[package.base] = package
def add_updated(self, package: Package) -> None:
"""
add new package to success built
Args:
package(Package): package built
"""
self._success[package.base] = package
self._updated[package.base] = package
# pylint: disable=protected-access
def merge(self, other: Result) -> Result:
def merge(self, other: Result) -> Self:
"""
merge other result into this one. This method assumes that other has fresh info about status and override it
@ -101,19 +151,35 @@ class Result:
other(Result): instance of the newest result
Returns:
Result: updated instance
Raises:
UnprocessedPackageStatusError: if there is previously failed package which is masked as success
Self: updated instance
"""
for base, package in other._failed.items():
if base in self._success:
del self._success[base]
self.add_failed(package)
for base, package in other._success.items():
if base in self._failed:
raise UnprocessedPackageStatusError(base)
self.add_success(package)
for status in self.STATUS_PRIORITIES:
new_packages: Iterable[Package] = getattr(other, f"_{status}", {}).values()
insert_package: Callable[[Package], None] = getattr(self, f"add_{status}")
for package in new_packages:
insert_package(package)
return self.refine()
def refine(self) -> Self:
"""
merge packages between different results (e.g. remove failed from added, etc.) removing duplicates
Returns:
Self: updated instance
"""
for index, base_status in enumerate(self.STATUS_PRIORITIES):
# extract top-level packages
base_packages: Iterable[str] = getattr(self, f"_{base_status}", {}).keys()
# extract packages for each bottom-level
for status in self.STATUS_PRIORITIES[index + 1:]:
packages: dict[str, Package] = getattr(self, f"_{status}", {})
# if there is top-level package in bottom-level, then remove it
for base in base_packages:
if base in packages:
del packages[base]
return self
# required for tests at least
@ -129,4 +195,7 @@ class Result:
"""
if not isinstance(other, Result):
return False
return self.success == other.success and self.failed == other.failed
return self._added == other._added \
and self._removed == other._removed \
and self._updated == other._updated \
and self._failed == other._failed

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPFound
from aiohttp.web import HTTPFound, HTTPNotFound
from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView
@ -40,5 +40,8 @@ class StaticView(BaseView):
Raises:
HTTPFound: on success response
HTTPNotFound: if path is invalid or unknown
"""
raise HTTPFound(f"/static{self.request.path}")
if self.request.path in self.ROUTES: # explicit validation
raise HTTPFound(f"/static{self.request.path}")
raise HTTPNotFound

View File

@ -228,7 +228,7 @@ def test_remove(application_packages: ApplicationPackages, mocker: MockerFixture
"""
must remove package
"""
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove")
executor_mock = mocker.patch("ahriman.core.repository.executor.Executor.process_remove", return_value=Result())
on_result_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages.on_result")
application_packages.remove([])

View File

@ -77,7 +77,7 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
args = _default_args(args)
args.now = True
result = Result()
result.add_success(package_ahriman)
result.add_updated(package_ahriman)
mocker.patch("ahriman.application.application.Application.add")
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)

View File

@ -18,7 +18,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
argparse.Namespace: generated arguments for these test cases
"""
args.parser = _parser
args.command = None
args.subcommand = None
return args
@ -39,7 +39,7 @@ def test_run_command(args: argparse.Namespace, configuration: Configuration, moc
must run command for specific subcommand
"""
args = _default_args(args)
args.command = "aur-search"
args.subcommand = "aur-search"
parse_mock = mocker.patch("argparse.ArgumentParser.parse_args")
_, repository_id = configuration.check_loaded()

View File

@ -10,6 +10,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@ -40,7 +41,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
"""
args = _default_args(args)
result = Result()
result.add_success(package_ahriman)
result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
extract_mock = mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[package_ahriman])
application_packages_mock = mocker.patch("ahriman.core.repository.repository.Repository.packages_depend_on",
@ -53,7 +54,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
Rebuild.run(args, repository_id, configuration, report=False)
extract_mock.assert_called_once_with(pytest.helpers.anyvar(int), args.status, from_database=args.from_database)
application_packages_mock.assert_called_once_with([package_ahriman], None)
application_mock.assert_called_once_with([package_ahriman], args.username, bump_pkgrel=args.increment)
application_mock.assert_called_once_with([package_ahriman], Packagers(args.username), bump_pkgrel=args.increment)
check_mock.assert_has_calls([MockCall(False, False), MockCall(False, False)])
on_start_mock.assert_called_once_with()

View File

@ -5,6 +5,7 @@ from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import call as MockCall
from urllib.parse import quote_plus as urlencode
from ahriman.application.handlers import Setup
from ahriman.core.configuration import Configuration
@ -145,7 +146,9 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "key",
args.sign_key),
MockCall("web", "port", str(args.web_port)),
MockCall("status", "address", f"http://127.0.0.1:{str(args.web_port)}"),
MockCall("web", "unix_socket", str(args.web_unix_socket)),
MockCall("status", "address", f"http+unix://{urlencode(str(args.web_unix_socket))}"),
MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)),
])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))

View File

@ -19,7 +19,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
argparse.Namespace: generated arguments for these test cases
"""
args.parser = _parser
args.command = []
args.subcommand = []
return args
@ -43,7 +43,7 @@ def test_run_check(args: argparse.Namespace, configuration: Configuration, mocke
must run command and check if command is unsafe
"""
args = _default_args(args)
args.command = ["clean"]
args.subcommand = ["clean"]
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"])
check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe")

View File

@ -44,7 +44,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
"""
args = _default_args(args)
result = Result()
result.add_success(package_ahriman)
result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty")

View File

@ -88,7 +88,7 @@ def test_clear(lock: Lock) -> None:
"""
must remove lock file
"""
lock.path = Path(tempfile.mktemp()) # nosec
lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.path.touch()
lock.clear()
@ -99,7 +99,7 @@ def test_clear_missing(lock: Lock) -> None:
"""
must not fail on lock removal if file is missing
"""
lock.path = Path(tempfile.mktemp()) # nosec
lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.clear()
@ -116,7 +116,7 @@ def test_create(lock: Lock) -> None:
"""
must create lock
"""
lock.path = Path(tempfile.mktemp()) # nosec
lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.create()
assert lock.path.is_file()
@ -127,7 +127,7 @@ def test_create_exception(lock: Lock) -> None:
"""
must raise exception if file already exists
"""
lock.path = Path(tempfile.mktemp()) # nosec
lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.path.touch()
with pytest.raises(DuplicateRunError):
@ -149,7 +149,7 @@ def test_create_unsafe(lock: Lock) -> None:
must not raise exception if force flag set
"""
lock.force = True
lock.path = Path(tempfile.mktemp()) # nosec
lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.path.touch()
lock.create()
@ -161,7 +161,7 @@ def test_watch(lock: Lock, mocker: MockerFixture) -> None:
must check if lock file exists
"""
wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait")
lock.path = Path(tempfile.mktemp()) # nosec
lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.watch()
wait_mock.assert_called_once_with(lock.path.is_file)

View File

@ -526,7 +526,7 @@ def result(package_ahriman: Package) -> Result:
Result: result test instance
"""
result = Result()
result.add_success(package_ahriman)
result.add_updated(package_ahriman)
return result

View File

@ -11,7 +11,7 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non
name = configuration.getpath("html", "template")
_, repository_id = configuration.check_loaded()
report = JinjaTemplate(repository_id, configuration, "html")
assert report.make_html(Result(success=[package_ahriman]), name)
assert report.make_html(Result(updated=[package_ahriman]), name)
def test_generate_from_path(configuration: Configuration, package_ahriman: Package) -> None:
@ -21,4 +21,4 @@ def test_generate_from_path(configuration: Configuration, package_ahriman: Packa
path = configuration.getpath("html", "templates") / configuration.get("html", "template")
_, repository_id = configuration.check_loaded()
report = JinjaTemplate(repository_id, configuration, "html")
assert report.make_html(Result(success=[package_ahriman]), path)
assert report.make_html(Result(updated=[package_ahriman]), path)

View File

@ -30,9 +30,30 @@ def test_load_dummy_client_disabled(configuration: Configuration) -> None:
assert not isinstance(Client.load(repository_id, configuration, report=False), WebClient)
def test_load_full_client(configuration: Configuration) -> None:
def test_load_dummy_client_disabled_in_configuration(configuration: Configuration) -> None:
"""
must load full client if settings set
must load dummy client if disabled in configuration
"""
configuration.set_option("web", "host", "localhost")
configuration.set_option("web", "port", "8080")
configuration.set_option("status", "enabled", "no")
_, repository_id = configuration.check_loaded()
assert not isinstance(Client.load(repository_id, configuration, report=True), WebClient)
def test_load_full_client_from_address(configuration: Configuration) -> None:
"""
must load full client by using address
"""
configuration.set_option("status", "address", "http://localhost:8080")
_, repository_id = configuration.check_loaded()
assert isinstance(Client.load(repository_id, configuration, report=True), WebClient)
def test_load_full_client_from_legacy_host(configuration: Configuration) -> None:
"""
must load full client if host and port settings set
"""
configuration.set_option("web", "host", "localhost")
configuration.set_option("web", "port", "8080")
@ -41,16 +62,16 @@ def test_load_full_client(configuration: Configuration) -> None:
assert isinstance(Client.load(repository_id, configuration, report=True), WebClient)
def test_load_full_client_from_address(configuration: Configuration) -> None:
def test_load_full_client_from_legacy_address(configuration: Configuration) -> None:
"""
must load full client by using address
must load full client by using legacy address
"""
configuration.set_option("web", "address", "http://localhost:8080")
_, repository_id = configuration.check_loaded()
assert isinstance(Client.load(repository_id, configuration, report=True), WebClient)
def test_load_full_client_from_unix_socket(configuration: Configuration) -> None:
def test_load_full_client_from_legacy_unix_socket(configuration: Configuration) -> None:
"""
must load full client by using unix socket
"""

View File

@ -15,42 +15,44 @@ from ahriman.models.package import Package
from ahriman.models.user import User
def test_session(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must create normal requests session
"""
login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login")
assert isinstance(web_client.session, requests.Session)
assert not isinstance(web_client.session, requests_unixsocket.Session)
login_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_session_unix_socket(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must create unix socket session
"""
login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login")
web_client.address = "http+unix://path"
assert isinstance(web_client.session, requests_unixsocket.Session)
login_mock.assert_not_called()
def test_parse_address(configuration: Configuration) -> None:
"""
must extract address correctly
"""
configuration.set_option("web", "host", "localhost")
configuration.set_option("web", "port", "8080")
assert WebClient.parse_address(configuration) == ("http://localhost:8080", False)
assert WebClient.parse_address(configuration) == ("web", "http://localhost:8080")
configuration.set_option("web", "address", "http://localhost:8081")
assert WebClient.parse_address(configuration) == ("http://localhost:8081", False)
assert WebClient.parse_address(configuration) == ("web", "http://localhost:8081")
configuration.set_option("web", "unix_socket", "/run/ahriman.sock")
assert WebClient.parse_address(configuration) == ("http+unix://%2Frun%2Fahriman.sock", True)
assert WebClient.parse_address(configuration) == ("web", "http+unix://%2Frun%2Fahriman.sock")
def test_create_session(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must create normal requests session
"""
login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login")
session = web_client._create_session(use_unix_socket=False)
assert isinstance(session, requests.Session)
assert not isinstance(session, requests_unixsocket.Session)
login_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_create_session_unix_socket(web_client: WebClient, mocker: MockerFixture) -> None:
"""
must create unix socket session
"""
login_mock = mocker.patch("ahriman.core.status.web_client.WebClient._login")
session = web_client._create_session(use_unix_socket=True)
assert isinstance(session, requests_unixsocket.Session)
login_mock.assert_not_called()
configuration.set_option("status", "address", "http://localhost:8082")
assert WebClient.parse_address(configuration) == ("status", "http://localhost:8082")
def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None:

View File

@ -8,13 +8,11 @@ from ahriman.core.upload.remote_service import RemoteService
from ahriman.models.package import Package
def test_session(remote_service: RemoteService, mocker: MockerFixture) -> None:
def test_session(remote_service: RemoteService) -> None:
"""
must generate ahriman session
"""
upload_mock = mocker.patch("ahriman.core.status.web_client.WebClient._create_session")
assert remote_service.session
upload_mock.assert_called_once_with(use_unix_socket=False)
assert remote_service.session == remote_service.client.session
def test_package_upload(remote_service: RemoteService, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -1,6 +1,3 @@
import pytest
from ahriman.core.exceptions import UnprocessedPackageStatusError
from ahriman.models.package import Package
from ahriman.models.result import Result
@ -18,7 +15,7 @@ def test_non_empty_success(package_ahriman: Package) -> None:
must be non-empty if there is success build
"""
result = Result()
result.add_success(package_ahriman)
result.add_updated(package_ahriman)
assert not result.is_empty
@ -37,11 +34,22 @@ def test_non_empty_full(package_ahriman: Package) -> None:
"""
result = Result()
result.add_failed(package_ahriman)
result.add_success(package_ahriman)
result.add_updated(package_ahriman)
assert not result.is_empty
def test_add_added(package_ahriman: Package) -> None:
"""
must add package to new packages list
"""
result = Result()
result.add_added(package_ahriman)
assert not result.failed
assert not result.removed
assert result.success == [package_ahriman]
def test_add_failed(package_ahriman: Package) -> None:
"""
must add package to failed list
@ -49,17 +57,30 @@ def test_add_failed(package_ahriman: Package) -> None:
result = Result()
result.add_failed(package_ahriman)
assert result.failed == [package_ahriman]
assert not result.removed
assert not result.success
def test_add_success(package_ahriman: Package) -> None:
def test_add_removed(package_ahriman: Package) -> None:
"""
must add package to removed list
"""
result = Result()
result.add_removed(package_ahriman)
assert not result.failed
assert result.removed == [package_ahriman]
assert not result.success
def test_add_updated(package_ahriman: Package) -> None:
"""
must add package to success list
"""
result = Result()
result.add_success(package_ahriman)
assert result.success == [package_ahriman]
result.add_updated(package_ahriman)
assert not result.failed
assert not result.removed
assert result.success == [package_ahriman]
def test_merge(package_ahriman: Package, package_python_schedule: Package) -> None:
@ -67,9 +88,9 @@ def test_merge(package_ahriman: Package, package_python_schedule: Package) -> No
must merge success packages
"""
left = Result()
left.add_success(package_ahriman)
left.add_updated(package_ahriman)
right = Result()
right.add_success(package_python_schedule)
right.add_updated(package_python_schedule)
result = left.merge(right)
assert result.success == [package_ahriman, package_python_schedule]
@ -81,7 +102,7 @@ def test_merge_failed(package_ahriman: Package) -> None:
must merge and remove failed packages from success list
"""
left = Result()
left.add_success(package_ahriman)
left.add_updated(package_ahriman)
right = Result()
right.add_failed(package_ahriman)
@ -90,28 +111,15 @@ def test_merge_failed(package_ahriman: Package) -> None:
assert not left.success
def test_merge_exception(package_ahriman: Package) -> None:
"""
must raise exception in case if package was failed
"""
left = Result()
left.add_failed(package_ahriman)
right = Result()
right.add_success(package_ahriman)
with pytest.raises(UnprocessedPackageStatusError):
left.merge(right)
def test_eq(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must return True for same objects
"""
left = Result()
left.add_success(package_ahriman)
left.add_updated(package_ahriman)
left.add_failed(package_python_schedule)
right = Result()
right.add_success(package_ahriman)
right.add_updated(package_ahriman)
right.add_failed(package_python_schedule)
assert left == right
@ -122,7 +130,7 @@ def test_eq_false(package_ahriman: Package) -> None:
must return False in case if lists do not match
"""
left = Result()
left.add_success(package_ahriman)
left.add_updated(package_ahriman)
right = Result()
right.add_failed(package_ahriman)
@ -144,7 +152,7 @@ def test_eq_false_success(package_ahriman: Package) -> None:
must return False in case if success does not match
"""
left = Result()
left.add_success(package_ahriman)
left.add_updated(package_ahriman)
assert left != Result()

View File

@ -24,8 +24,19 @@ def test_routes() -> None:
async def test_get(client_with_auth: TestClient) -> None:
"""
must generate status page correctly (/)
must redirect favicon to static files
"""
response = await client_with_auth.get("/favicon.ico", allow_redirects=False)
assert response.status == 302
assert response.headers["Location"] == "/static/favicon.ico"
async def test_get_not_found(client_with_auth: TestClient) -> None:
"""
must raise not found if path is invalid
"""
for route in client_with_auth.app.router.routes():
if hasattr(route.handler, "ROUTES"):
route.handler.ROUTES = []
response = await client_with_auth.get("/favicon.ico", allow_redirects=False)
assert response.status == 404