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 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. 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 ### 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. 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 .DEFAULT_GOAL := archlinux
PROJECT := ahriman PROJECT := ahriman
@ -37,7 +37,7 @@ html: specification
rm -rf docs/html rm -rf docs/html
tox -e 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 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 commit -m "Release $(VERSION)"
git tag "$(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. * ``database`` - path to SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. * ``include`` - path to directory with configuration files overrides, string, optional.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference. * ``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 ``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. * ``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`` - 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. * ``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 ``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). * ``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. * ``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 ``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. * ``host`` - host to bind, string, optional.
* ``index_url`` - full url of the repository index page, 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. * ``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, integer, optional.
* ``port`` - port to bind, int, optional.
* ``static_path`` - path to directory with static files, string, required. * ``static_path`` - path to directory with static files, string, required.
* ``templates`` - path to templates directories, space separated list of strings, 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`` - 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. * ``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, integer, optional.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, int, optional.
``keyring`` group ``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. * ``link_path`` - prefix for HTML links, string, required.
* ``no_empty_report`` - skip report generation for empty packages list, boolean, optional, default ``yes``. * ``no_empty_report`` - skip report generation for empty packages list, boolean, optional, default ``yes``.
* ``password`` - SMTP password to authenticate, string, optional. * ``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. * ``receivers`` - SMTP receiver addresses, space separated list of strings, required.
* ``sender`` - SMTP sender address, string, required. * ``sender`` - SMTP sender address, string, required.
* ``ssl`` - SSL mode for SMTP connection, one of ``ssl``, ``starttls``, ``disabled``, optional, default ``disabled``. * ``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``. * ``aur`` - check for AUR packages updates, boolean, optional, default ``no``.
* ``local`` - check for local 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``. * ``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 ``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`` - Jinja2 template name, string, required.
* ``template_type`` - ``parse_mode`` to be passed to telegram API, one of ``MarkdownV2``, ``HTML``, ``Markdown``, string, optional, default ``HTML``. * ``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. * ``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 ``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). #. 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). * ``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). * ``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``. * ``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. 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. * ``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 ``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. * ``type`` - type of the upload, string, optional, must be set to ``s3`` if exists.
* ``access_key`` - AWS access key ID, string, required. * ``access_key`` - AWS access key ID, string, required.
* ``bucket`` - bucket name (e.g. ``bucket``), 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. * ``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. * ``region`` - bucket region (e.g. ``eu-central-1``), string, required.
* ``secret_key`` - AWS secret access key, string, required. * ``secret_key`` - AWS secret access key, string, required.

View File

@ -869,12 +869,12 @@ Worker nodes configuration
.. code-block:: ini .. code-block:: ini
[web] [status]
address = master.example.com address = https://master.example.com
username = worker-user username = worker-user
password = very-secure-password 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: 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 .. code-block:: ini
[web] [status]
address = http://172.17.0.1:8080 address = http://172.17.0.1:8080
username = worker-user username = worker-user
password = very-secure-password password = very-secure-password
@ -1142,7 +1142,7 @@ How to enable basic authorization
.. code-block:: ini .. code-block:: ini
[web] [status]
username = api username = api
password = pa55w0rd password = pa55w0rd

View File

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

View File

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

View File

@ -182,7 +182,12 @@
const description = response.find(Boolean); const description = response.find(Boolean);
const packages = Object.keys(description.package.packages); const packages = Object.keys(description.package.packages);
const aurUrl = description.package.remote.web_url; 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()}`); 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_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_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_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help') _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 .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS
.B ahriman .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 .SH DESCRIPTION
ArcH linux ReposItory MANager ArcH linux ReposItory MANager
@ -44,15 +44,15 @@ filter by target repository
\fB\-\-unsafe\fR \fB\-\-unsafe\fR
allow to run ahriman as non\-ahriman user. Some actions might be unavailable 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 .TP
\fB\-\-wait\-timeout\fR \fI\,WAIT_TIMEOUT\/\fR \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 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 zero value, the application will wait infinitely
.TP
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
.SH .SH
COMMAND COMMAND
.TP .TP
@ -208,22 +208,22 @@ sort field by this field. In case if two packages have the same value of the spe
by name by name
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR .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 list unsafe commands as defined in default args
.TP .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 instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1
otherwise otherwise
.SH COMMAND \fI\,'ahriman help'\/\fR .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 show help message for application or command and exit
.TP .TP
\fBcommand\fR \fBsubcommand\fR
show help message for specific command show help message for specific command
.SH COMMAND \fI\,'ahriman help\-updates'\/\fR .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:" {--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
{-r,--repository}"[filter by target repository (default\: None)]:repository:" {-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)]" "--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]" "(- : *)"{-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=( _shtab_ahriman_add_options=(

View File

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

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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("--repository-id", help=argparse.SUPPRESS)
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable", parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true") 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 " 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. " "immediate application run even if there is lock file. "
"In case of zero value, the application will wait infinitely", "In case of zero value, the application will wait infinitely",
type=int, default=-1) 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") 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", parser = root.add_parser("help-commands-unsafe", help="list unsafe commands",
description="list unsafe commands as defined in default args", formatter_class=_formatter) 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 " 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="*") "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, parser.set_defaults(handler=handlers.UnsafeCommands, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True, parser=_parser) repository="", unsafe=True, parser=_parser)
return parser return parser
@ -198,7 +198,7 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("help", help="show help message", parser = root.add_parser("help", help="show help message",
description="show help message for application or command and exit", description="show help message for application or command and exit",
formatter_class=_formatter) 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="", parser.set_defaults(handler=handlers.Help, architecture="", lock=None, quiet=True, report=False, repository="",
unsafe=True, parser=_parser) unsafe=True, parser=_parser)
return parser return parser

View File

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

View File

@ -44,7 +44,7 @@ class Help(Handler):
report(bool): force enable or disable reporting report(bool): force enable or disable reporting
""" """
parser: argparse.ArgumentParser = args.parser() parser: argparse.ArgumentParser = args.parser()
if args.command is None: if args.subcommand is None:
parser.parse_args(["--help"]) parser.parse_args(["--help"])
else: 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.core.configuration import Configuration
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -55,7 +56,7 @@ class Rebuild(Handler):
application.print_updates(updates, log_fn=print) application.print_updates(updates, log_fn=print)
return 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) Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod @staticmethod

View File

@ -21,6 +21,7 @@ import argparse
from pathlib import Path from pathlib import Path
from pwd import getpwuid from pwd import getpwuid
from urllib.parse import quote_plus as urlencode
from ahriman.application.application import Application from ahriman.application.application import Application
from ahriman.application.handlers import Handler from ahriman.application.handlers import Handler
@ -128,8 +129,12 @@ class Setup(Handler):
if args.web_port is not None: if args.web_port is not None:
configuration.set_option("web", "port", str(args.web_port)) 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: 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: if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20)) configuration.set_option("auth", "salt", User.generate_password(20))

View File

@ -46,8 +46,8 @@ class UnsafeCommands(Handler):
""" """
parser = args.parser() parser = args.parser()
unsafe_commands = UnsafeCommands.get_unsafe_commands(parser) unsafe_commands = UnsafeCommands.get_unsafe_commands(parser)
if args.command: if args.subcommand:
UnsafeCommands.check_unsafe(args.command, unsafe_commands, parser) UnsafeCommands.check_unsafe(args.subcommand, unsafe_commands, parser)
else: else:
for command in unsafe_commands: for command in unsafe_commands:
StringPrinter(command)(verbose=True) 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": { "web": {
"type": "dict", "type": "dict",
"schema": { "schema": {

View File

@ -316,21 +316,6 @@ class UnknownPackageError(ValueError):
ValueError.__init__(self, f"Package base {package_base} is unknown") 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): class UnsafeRunError(RuntimeError):
""" """
exception which will be raised in case if user is not owner of repository 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: if (handler := next((handler for handler in root.handlers if isinstance(handler, cls)), None)) is not None:
return handler # there is already registered instance 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) handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
root.addHandler(handler) root.addHandler(handler)

View File

@ -120,6 +120,6 @@ class Email(Report, JinjaTemplate):
text = self.make_html(result, self.template) text = self.make_html(result, self.template)
attachments = {} attachments = {}
if self.template_full is not None: 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) self._send(text, attachments)

View File

@ -58,5 +58,5 @@ class HTML(Report, JinjaTemplate):
packages(list[Package]): list of packages to generate report packages(list[Package]): list of packages to generate report
result(Result): build result 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") self.report_path.write_text(html, encoding="utf8")

View File

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

View File

@ -49,16 +49,19 @@ class Client:
""" """
if not report: if not report:
return Client() 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) host = configuration.get("web", "host", fallback=None)
port = configuration.getint("web", "port", fallback=None) port = configuration.getint("web", "port", fallback=None)
socket = configuration.get("web", "unix_socket", 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 # 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`, if address or legacy_address or (host and port) or socket:
# but it will totally break used experience
if address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
return WebClient(repository_id, configuration) return WebClient(repository_id, configuration)
return Client() return Client()

View File

@ -22,7 +22,7 @@ import logging
import requests import requests
from functools import cached_property 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 import __version__
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -42,7 +42,6 @@ class WebClient(Client, SyncHttpClient):
Attributes: Attributes:
address(str): address of the web service address(str): address of the web service
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
use_unix_socket(bool): use websocket or not
""" """
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
@ -53,11 +52,13 @@ class WebClient(Client, SyncHttpClient):
repository_id(RepositoryId): repository unique identifier repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance configuration(Configuration): configuration instance
""" """
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False) section, self.address = self.parse_address(configuration)
SyncHttpClient.__init__(self, configuration, "web", suppress_errors=suppress_errors) 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.repository_id = repository_id
self.address, self.use_unix_socket = self.parse_address(configuration)
@cached_property @cached_property
def session(self) -> requests.Session: def session(self) -> requests.Session:
@ -67,41 +68,7 @@ class WebClient(Client, SyncHttpClient):
Returns: Returns:
request.Session: created session object request.Session: created session object
""" """
return self._create_session(use_unix_socket=self.use_unix_socket) if urlparse(self.address).scheme == "http+unix":
@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:
import requests_unixsocket # type: ignore[import-untyped] import requests_unixsocket # type: ignore[import-untyped]
session: requests.Session = requests_unixsocket.Session() session: requests.Session = requests_unixsocket.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}" session.headers["User-Agent"] = f"ahriman/{__version__}"
@ -113,6 +80,33 @@ class WebClient(Client, SyncHttpClient):
return session 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: def _login(self, session: requests.Session) -> None:
""" """
process login to the service process login to the service

View File

@ -65,6 +65,14 @@ class TriggerLoader(LazyLogging):
self._on_stop_requested = False self._on_stop_requested = False
self.triggers: list[Trigger] = [] 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 @classmethod
def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self: def load(cls, repository_id: RepositoryId, configuration: Configuration) -> Self:
""" """
@ -257,11 +265,3 @@ class TriggerLoader(LazyLogging):
for trigger in self.triggers: for trigger in self.triggers:
with self.__execute_trigger(trigger): with self.__execute_trigger(trigger):
trigger.on_stop() 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 __future__ import annotations
from collections.abc import Iterable from collections.abc import Iterable, Callable
from typing import Any from typing import Any, Self
from ahriman.core.exceptions import UnprocessedPackageStatusError
from ahriman.models.package import Package from ahriman.models.package import Package
class Result: class Result:
""" """
build result class holder 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 default constructor
Args: 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) failed(Iterable[Package] | None, optional): initial list of failed packages (Default value = None)
""" """
success = success or [] added = added or []
self._success = {package.base: package for package in success} 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 [] failed = failed or []
self._failed = {package.base: package for package in failed} self._failed = {package.base: package for package in failed}
@ -62,7 +84,17 @@ class Result:
Returns: Returns:
bool: True in case if success list is empty and False otherwise 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 @property
def success(self) -> list[Package]: def success(self) -> list[Package]:
@ -72,7 +104,16 @@ class Result:
Returns: Returns:
list[Package]: list of packages with success result 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: def add_failed(self, package: Package) -> None:
""" """
@ -83,17 +124,26 @@ class Result:
""" """
self._failed[package.base] = package 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 add new package to success built
Args: Args:
package(Package): package built package(Package): package built
""" """
self._success[package.base] = package self._updated[package.base] = package
# pylint: disable=protected-access # 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 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 other(Result): instance of the newest result
Returns: Returns:
Result: updated instance Self: updated instance
Raises:
UnprocessedPackageStatusError: if there is previously failed package which is masked as success
""" """
for base, package in other._failed.items(): for status in self.STATUS_PRIORITIES:
if base in self._success: new_packages: Iterable[Package] = getattr(other, f"_{status}", {}).values()
del self._success[base] insert_package: Callable[[Package], None] = getattr(self, f"add_{status}")
self.add_failed(package) for package in new_packages:
for base, package in other._success.items(): insert_package(package)
if base in self._failed:
raise UnprocessedPackageStatusError(base) return self.refine()
self.add_success(package)
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 return self
# required for tests at least # required for tests at least
@ -129,4 +195,7 @@ class Result:
""" """
if not isinstance(other, Result): if not isinstance(other, Result):
return False 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 # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # 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.models.user_access import UserAccess
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -40,5 +40,8 @@ class StaticView(BaseView):
Raises: Raises:
HTTPFound: on success response 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 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") on_result_mock = mocker.patch("ahriman.application.application.application_packages.ApplicationPackages.on_result")
application_packages.remove([]) application_packages.remove([])

View File

@ -77,7 +77,7 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
args = _default_args(args) args = _default_args(args)
args.now = True args.now = True
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
mocker.patch("ahriman.application.application.Application.add") mocker.patch("ahriman.application.application.Application.add")
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result) 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 argparse.Namespace: generated arguments for these test cases
""" """
args.parser = _parser args.parser = _parser
args.command = None args.subcommand = None
return args return args
@ -39,7 +39,7 @@ def test_run_command(args: argparse.Namespace, configuration: Configuration, moc
must run command for specific subcommand must run command for specific subcommand
""" """
args = _default_args(args) args = _default_args(args)
args.command = "aur-search" args.subcommand = "aur-search"
parse_mock = mocker.patch("argparse.ArgumentParser.parse_args") parse_mock = mocker.patch("argparse.ArgumentParser.parse_args")
_, repository_id = configuration.check_loaded() _, 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.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result from ahriman.models.result import Result
@ -40,7 +41,7 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
""" """
args = _default_args(args) args = _default_args(args)
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
extract_mock = mocker.patch("ahriman.application.handlers.Rebuild.extract_packages", return_value=[package_ahriman]) 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", 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) 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) 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_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)]) check_mock.assert_has_calls([MockCall(False, False), MockCall(False, False)])
on_start_mock.assert_called_once_with() on_start_mock.assert_called_once_with()

View File

@ -5,6 +5,7 @@ from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from typing import Any from typing import Any
from unittest.mock import call as MockCall from unittest.mock import call as MockCall
from urllib.parse import quote_plus as urlencode
from ahriman.application.handlers import Setup from ahriman.application.handlers import Setup
from ahriman.core.configuration import Configuration 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", MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "key",
args.sign_key), args.sign_key),
MockCall("web", "port", str(args.web_port)), 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("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)), MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)),
]) ])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int)) 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 argparse.Namespace: generated arguments for these test cases
""" """
args.parser = _parser args.parser = _parser
args.command = [] args.subcommand = []
return args 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 must run command and check if command is unsafe
""" """
args = _default_args(args) args = _default_args(args)
args.command = ["clean"] args.subcommand = ["clean"]
commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands", commands_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.get_unsafe_commands",
return_value=["command"]) return_value=["command"])
check_mock = mocker.patch("ahriman.application.handlers.UnsafeCommands.check_unsafe") 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) args = _default_args(args)
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository) mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result) application_mock = mocker.patch("ahriman.application.application.Application.update", return_value=result)
check_mock = mocker.patch("ahriman.application.handlers.Handler.check_if_empty") 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 must remove lock file
""" """
lock.path = Path(tempfile.mktemp()) # nosec lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.path.touch() lock.path.touch()
lock.clear() lock.clear()
@ -99,7 +99,7 @@ def test_clear_missing(lock: Lock) -> None:
""" """
must not fail on lock removal if file is missing 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() lock.clear()
@ -116,7 +116,7 @@ def test_create(lock: Lock) -> None:
""" """
must create lock must create lock
""" """
lock.path = Path(tempfile.mktemp()) # nosec lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.create() lock.create()
assert lock.path.is_file() assert lock.path.is_file()
@ -127,7 +127,7 @@ def test_create_exception(lock: Lock) -> None:
""" """
must raise exception if file already exists must raise exception if file already exists
""" """
lock.path = Path(tempfile.mktemp()) # nosec lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.path.touch() lock.path.touch()
with pytest.raises(DuplicateRunError): with pytest.raises(DuplicateRunError):
@ -149,7 +149,7 @@ def test_create_unsafe(lock: Lock) -> None:
must not raise exception if force flag set must not raise exception if force flag set
""" """
lock.force = True lock.force = True
lock.path = Path(tempfile.mktemp()) # nosec lock.path = Path(tempfile.gettempdir()) / "ahriman-test.lock"
lock.path.touch() lock.path.touch()
lock.create() lock.create()
@ -161,7 +161,7 @@ def test_watch(lock: Lock, mocker: MockerFixture) -> None:
must check if lock file exists must check if lock file exists
""" """
wait_mock = mocker.patch("ahriman.models.waiter.Waiter.wait") 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() lock.watch()
wait_mock.assert_called_once_with(lock.path.is_file) 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 test instance
""" """
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
return result return result

View File

@ -11,7 +11,7 @@ def test_generate(configuration: Configuration, package_ahriman: Package) -> Non
name = configuration.getpath("html", "template") name = configuration.getpath("html", "template")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
report = JinjaTemplate(repository_id, configuration, "html") 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: 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") path = configuration.getpath("html", "templates") / configuration.get("html", "template")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
report = JinjaTemplate(repository_id, configuration, "html") 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) 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", "host", "localhost")
configuration.set_option("web", "port", "8080") 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) 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") configuration.set_option("web", "address", "http://localhost:8080")
_, repository_id = configuration.check_loaded() _, repository_id = configuration.check_loaded()
assert isinstance(Client.load(repository_id, configuration, report=True), WebClient) 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 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 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: def test_parse_address(configuration: Configuration) -> None:
""" """
must extract address correctly must extract address correctly
""" """
configuration.set_option("web", "host", "localhost") configuration.set_option("web", "host", "localhost")
configuration.set_option("web", "port", "8080") 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") 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") 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")
configuration.set_option("status", "address", "http://localhost:8082")
def test_create_session(web_client: WebClient, mocker: MockerFixture) -> None: assert WebClient.parse_address(configuration) == ("status", "http://localhost:8082")
"""
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()
def test_login(web_client: WebClient, user: User, mocker: MockerFixture) -> None: 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 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 must generate ahriman session
""" """
upload_mock = mocker.patch("ahriman.core.status.web_client.WebClient._create_session") assert remote_service.session == remote_service.client.session
assert remote_service.session
upload_mock.assert_called_once_with(use_unix_socket=False)
def test_package_upload(remote_service: RemoteService, package_ahriman: Package, mocker: MockerFixture) -> None: 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.package import Package
from ahriman.models.result import Result 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 must be non-empty if there is success build
""" """
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
assert not result.is_empty assert not result.is_empty
@ -37,11 +34,22 @@ def test_non_empty_full(package_ahriman: Package) -> None:
""" """
result = Result() result = Result()
result.add_failed(package_ahriman) result.add_failed(package_ahriman)
result.add_success(package_ahriman) result.add_updated(package_ahriman)
assert not result.is_empty 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: def test_add_failed(package_ahriman: Package) -> None:
""" """
must add package to failed list must add package to failed list
@ -49,17 +57,30 @@ def test_add_failed(package_ahriman: Package) -> None:
result = Result() result = Result()
result.add_failed(package_ahriman) result.add_failed(package_ahriman)
assert result.failed == [package_ahriman] assert result.failed == [package_ahriman]
assert not result.removed
assert not result.success 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 must add package to success list
""" """
result = Result() result = Result()
result.add_success(package_ahriman) result.add_updated(package_ahriman)
assert result.success == [package_ahriman]
assert not result.failed assert not result.failed
assert not result.removed
assert result.success == [package_ahriman]
def test_merge(package_ahriman: Package, package_python_schedule: Package) -> None: 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 must merge success packages
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
right = Result() right = Result()
right.add_success(package_python_schedule) right.add_updated(package_python_schedule)
result = left.merge(right) result = left.merge(right)
assert result.success == [package_ahriman, package_python_schedule] 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 must merge and remove failed packages from success list
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
right = Result() right = Result()
right.add_failed(package_ahriman) right.add_failed(package_ahriman)
@ -90,28 +111,15 @@ def test_merge_failed(package_ahriman: Package) -> None:
assert not left.success 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: def test_eq(package_ahriman: Package, package_python_schedule: Package) -> None:
""" """
must return True for same objects must return True for same objects
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
left.add_failed(package_python_schedule) left.add_failed(package_python_schedule)
right = Result() right = Result()
right.add_success(package_ahriman) right.add_updated(package_ahriman)
right.add_failed(package_python_schedule) right.add_failed(package_python_schedule)
assert left == right 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 must return False in case if lists do not match
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
right = Result() right = Result()
right.add_failed(package_ahriman) 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 must return False in case if success does not match
""" """
left = Result() left = Result()
left.add_success(package_ahriman) left.add_updated(package_ahriman)
assert left != Result() assert left != Result()

View File

@ -24,8 +24,19 @@ def test_routes() -> None:
async def test_get(client_with_auth: TestClient) -> 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) response = await client_with_auth.get("/favicon.ico", allow_redirects=False)
assert response.status == 302 assert response.status == 302
assert response.headers["Location"] == "/static/favicon.ico" 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