mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-27 14:22:10 +00:00
Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
b050c409cf | |||
d77cf7c4bb | |||
e03fcbfab5 | |||
62dd77317d | |||
95056cfbe7 | |||
2d31a415ce | |||
5cbeec40f8 | |||
4a644fc80f | |||
d1aeeda975 |
@ -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.
|
||||
|
4
Makefile
4
Makefile
@ -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 |
@ -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.
|
10
docs/faq.rst
10
docs/faq.rst
@ -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
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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()}`);
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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=(
|
||||
|
@ -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 = []
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"])
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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([])
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user