Compare commits

..

34 Commits

Author SHA1 Message Date
415fcf58ce Release 2.20.0rc4 2026-02-21 12:52:51 +02:00
17a2d6362c style: replace """ with " for single line strings 2026-02-21 12:51:58 +02:00
e6275de4ed fix: handle class overrides for retry polices correctly
Previous implementation didn't work as intended because there was still
override in init. Current implementation instead of playing with
guessing separates default and instance setttings. Also update test
cases to handle this scenario correctly
2026-02-21 12:40:11 +02:00
4d009cba6d Release 2.20.0rc3 2026-02-20 20:56:05 +02:00
f6defbf90d fix: rollback samesite option to Lax, because of broken OAuth 2026-02-20 20:54:37 +02:00
c4fefa6071 Release 2.20.0rc2 2026-02-20 16:30:37 +02:00
b0f1828ae7 feat: add retry policy 2026-02-20 16:24:14 +02:00
dec025b45a feat: raise OptionError on missing OAuth provider class instead of generic AttributeError 2026-02-19 10:19:03 +02:00
89008e5350 fix: use context manager for selector and smtp session 2026-02-19 10:19:03 +02:00
422196d413 fix: force data filter for tar archive extraction
(python3.14 default anyway)
2026-02-19 10:19:03 +02:00
6fe2eade26 feat: (more) secure cookies 2026-02-19 10:19:03 +02:00
5266f54257 fix: speedup table reload by updating only changed statuses
it has been found that on big (>100) repos it starts lagging on reload.
This commit adds guard to avoid updating rows whose package statuses
were not changed
2026-02-19 10:19:03 +02:00
bbf9e38fda Release 2.20.0rc1 2026-02-18 13:34:08 +02:00
ba80a91d95 feat: implement CSRF protection 2026-02-18 13:34:08 +02:00
536d040a6a feat: handle only unknownpackageerror on aur load 2026-02-18 13:34:08 +02:00
bed8752f3a fix: filter logs by repository (twice) before rotation 2026-02-18 13:34:05 +02:00
4093ca8986 fix: do not clear queue on queue fetch failures 2026-02-18 13:34:03 +02:00
f027155885 docs: correct docstring for list_flatmap method 2026-02-18 13:34:03 +02:00
443d4ae667 fix: correct vcs definition for cvs packages 2026-02-18 13:34:00 +02:00
c8f7fa8c51 fix: load gitremote triggers configuration schema from non-standard
paths
2026-02-18 13:33:57 +02:00
93c36fb429 docs: update documentation for the lasts archive feature 2026-02-16 22:07:34 +02:00
2d6d42f969 feat: archive package tree implementation (#153)
* store built packages in archive tree instead of repository

* write tests to support new changes

* implement atomic_move method, move files only with lock

* use generic packages tree for all repos

* lookup through archive packages before build

* add archive trigger

* add archive trigger

* regenerate docs

* gpg loader fix

* support requires repostory flag

* drop excess REQUIRES_REPOSITORY

* simplify symlionk creation

* remove generators

* fix sttyle

* add separate function for symlinks creation

* fix rebase

* add note about slicing

* smol refactoring of archive_tree class

* remove duplicate code

* fix typos

* few review fixes

* monor fixes and typos

* clean empty directories

* remove side effect from getter

* drop recursive remove

* ensure_exists now accepts only argument

* add package like guard to symlinks fix

* speedup archive_lookup processing by iterrupting cycle

* remove custom filelock

* fix naming

* remove remove flag from repo

* review fixes

* restore wrapper around filelock

* extract repository explorer to separate class

* docs update

* fix ide findings
2026-02-16 00:12:51 +02:00
6a2454548d refactor: drop some methods from package class into separated wrappers 2026-02-11 03:05:49 +02:00
389bad6725 fix: use effective uid instead of uid 2026-02-03 16:38:13 +02:00
5738b8b911 fix: rewrite preserver_owner method complitely
Previous implementation was somewhat working in the most) scenarios, but
was super slow to handle permissions. However, it is actually very
limited operations in which the application can do anything, so it is
much easier to just drop privileged user to normal one
2026-02-03 15:27:19 +02:00
5ac2e3de19 fix: handle permissionerror during walking over tree
Previously it tried to look into 700 directories (e.g. .gnupg) which
breaks running as non-ahriman user
2026-02-02 22:16:15 +02:00
799dc73d8a feat: update package status to success in case if package is up-to-date 2026-01-26 01:56:30 +02:00
4e79cbf71a fix: fallback to package name for missing bases in archive
package zoom is being generated without base, leading to None there

Closes #155
2026-01-22 16:34:19 +02:00
4fa5d55317 type: replace generator return type with iterator 2026-01-15 15:22:03 +02:00
a7fa3b90e4 type: fix typing in some modules 2026-01-14 14:28:31 +02:00
ce07cda8ab fix: pass underlying exception on extensionerror 2026-01-14 14:06:34 +02:00
00c4f32294 fix: correct exception type on repository id comparison 2026-01-14 13:59:07 +02:00
49cf91ea52 chore: copyright update 2026-01-08 02:50:37 +02:00
4a8430dc67 build: allow to rebuild images manually 2026-01-08 02:45:31 +02:00
117 changed files with 3648 additions and 2711 deletions

View File

@@ -10,7 +10,7 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
# refresh the image
pacman -Syyu --noconfirm
# main dependencies
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-inflection python-pyelftools python-requests python-systemd sudo
pacman -S --noconfirm devtools git pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
# make dependencies
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
# optional dependencies

View File

@@ -220,6 +220,7 @@ Again, the most checks can be performed by `tox` command, though some additional
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
* `Generator` return type is not allowed. Generator functions must return generic `Iterator` object. Documentation should be described as `Yields`, however, because of pylint checks. Unfortunately, `Iterable` return type is not available for generators also, because of specific `contextlib.contextmanager` case.
### Other checks

View File

@@ -24,7 +24,8 @@ RUN pacman -S --noconfirm --asdeps \
devtools \
git \
pyalpm \
python-bcrypt \
python-bcrypt \
python-filelock \
python-inflection \
python-pyelftools \
python-requests \

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,14 @@ ahriman.core.build\_tools.package\_archive module
:no-undoc-members:
:show-inheritance:
ahriman.core.build\_tools.package\_version module
-------------------------------------------------
.. automodule:: ahriman.core.build_tools.package_version
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.build\_tools.sources module
----------------------------------------

View File

@@ -28,6 +28,14 @@ ahriman.core.repository.executor module
:no-undoc-members:
:show-inheritance:
ahriman.core.repository.explorer module
---------------------------------------
.. automodule:: ahriman.core.repository.explorer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.repository.package\_info module
--------------------------------------------

View File

@@ -120,6 +120,20 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
/var/lib/ahriman/
├── ahriman.db
├── archive
│ ├── packages
│ │ └── a
│ │ └── ahriman
│ │ └── ahriman-2.0.0-1-any.pkg.tar.zst
│ └── repos
│ └── 2026
│ └── 01
│ └── 01
│ └── aur
│ └── x86_64
│ ├── ahriman-2.0.0-1-any.pkg.tar.zst -> ../../../../../../packages/a/ahriman/ahriman-2.0.0-1-any.pkg.tar.zst
│ ├── aur.db -> aur.db.tar.gz
│ └── aur.db.tar.gz
├── cache
├── chroot
│ └── aur
@@ -139,6 +153,7 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
└── repository
└── aur
└── x86_64
├── ahriman-2.0.0-1-any.pkg.tar.zst -> ../../../archive/packages/a/ahriman/ahriman-2.0.0-1-any.pkg.tar.zst
├── aur.db -> aur.db.tar.gz
├── aur.db.tar.gz
├── aur.files -> aur.files.tar.gz
@@ -146,11 +161,18 @@ Having default root as ``/var/lib/ahriman`` (differs from container though), the
There are multiple subdirectories, some of them are commons for any repository, but some of them are not.
* ``archive`` is the package archive directory. It is common for all repositories and architectures and contains two subdirectories:
* ``archive/packages/{first_letter}/{package_base}`` stores the actual built package files and their signatures.
* ``archive/repos/{YYYY}/{MM}/{DD}/{repository}/{architecture}`` contains daily repository snapshots. Each snapshot is a repository database with symlinks pointing to the corresponding packages in the ``archive/packages`` tree.
The archive also allows the build process to skip rebuilding a package if a matching version already exists.
* ``cache`` is a directory with locally stored PKGBUILD's and VCS packages. It is common for all repositories and architectures.
* ``chroot/{repository}`` is a chroot directory for ``devtools``. It is specific for each repository, but shared for different architectures inside (the ``devtools`` handles architectures automatically).
* ``packages/{repository}/{architecture}`` is a directory with prebuilt packages. When a package is built, first it will be uploaded to this directory and later will be handled by update process. It is architecture and repository specific.
* ``pacman/{repository}/{architecture}`` is the repository and architecture specific caches for pacman's databases.
* ``repository/{repository}/{architecture}`` is a repository packages directory.
* ``repository/{repository}/{architecture}`` is a repository packages directory. Package files in this directory are symlinks to the archive.
Normally you should avoid direct interaction with the application tree. For tree migration process refer to the :doc:`migration notes <migrations/index>`.

View File

@@ -97,12 +97,14 @@ libalpm and AUR related configuration. Group name can refer to architecture, e.g
* ``sync_files_database`` - download files database from mirror, boolean, required.
* ``use_ahriman_cache`` - use local pacman package cache instead of system one, boolean, required. With this option enabled you might want to refresh database periodically (available as additional flag for some subcommands). If set to ``no``, databases must be synchronized manually.
``archive`` group
-----------------
``aur`` group
-------------
Describes settings for packages archives management extensions.
Archlinux User Repository related configuration.
* ``keep_built_packages`` - keep this amount of built packages with different versions, integer, required. ``0`` (or negative number) will effectively disable archives removal.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``auth`` group
--------------
@@ -165,7 +167,9 @@ Reporting to web service related settings. In most cases there is fallback to we
* ``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.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``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.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
@@ -189,6 +193,13 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
* ``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.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
``archive`` group
-----------------
Describes settings for packages archives management extensions.
* ``keep_built_packages`` - keep this amount of built packages with different versions, integer, required. ``0`` will effectively disable archives removal.
``keyring`` group
-----------------
@@ -208,12 +219,12 @@ Keyring generator plugin
* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
``housekeeping`` group
----------------------
``logs-rotation`` group
-----------------------
This section describes settings for the ``ahriman.core.housekeeping.LogsRotationTrigger`` plugin.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, required. Logs will be cleared at the end of each process.
``mirrorlist`` group
--------------------
@@ -250,6 +261,7 @@ Available options are:
Remote pull trigger
^^^^^^^^^^^^^^^^^^^
* ``type`` - type of the pull, string, optional, must be set to ``gitremote`` if exists.
* ``pull_url`` - URL of the remote repository from which PKGBUILDs can be pulled before build process, string, required.
* ``pull_branch`` - branch of the remote repository from which PKGBUILDs can be pulled before build process, string, optional, default is ``master``.
@@ -270,6 +282,7 @@ Available options are:
Remote push trigger
^^^^^^^^^^^^^^^^^^^
* ``type`` - type of the push, string, optional, must be set to ``gitremote`` if exists.
* ``commit_email`` - git commit email, string, optional, default is ``ahriman@localhost``.
* ``commit_user`` - git commit user, string, optional, default is ``ahriman``.
* ``push_url`` - URL of the remote repository to which PKGBUILDs should be pushed after build process, string, required.
@@ -365,6 +378,8 @@ Section name must be either ``telegram`` (plus optional architecture name, e.g.
* ``chat_id`` - telegram chat id, either string with ``@`` or integer value, required.
* ``homepage`` - link to homepage, string, optional.
* ``link_path`` - prefix for HTML links, string, required.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``rss_url`` - link to RSS feed, string, optional.
* ``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``.
@@ -390,6 +405,7 @@ Type will be read from several sources:
This feature requires GitHub key creation (see below). Section name must be either ``github`` (plus optional architecture name, e.g. ``github:x86_64``) or random name with ``type`` set.
* ``type`` - type of the upload, string, optional, must be set to ``github`` if exists.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``owner`` - GitHub repository owner, string, required.
* ``password`` - created GitHub API key. In order to create it do the following:
@@ -399,6 +415,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).
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``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``.
@@ -409,6 +426,8 @@ 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.
* ``max_retries`` - maximum amount of retries of HTTP requests, integer, optional, default ``0``.
* ``retry_backoff`` - retry exponential backoff, float, optional, default ``0.0``.
* ``timeout`` - HTTP request timeout in seconds, integer, optional, default is ``30``.
``rsync`` type

View File

@@ -40,6 +40,8 @@ docutils==0.21.2
# sphinx
# sphinx-argparse
# sphinx-rtd-theme
filelock==3.24.0
# via ahriman (pyproject.toml)
frozenlist==1.6.0
# via
# aiohttp

View File

@@ -1,7 +1,7 @@
Triggers
========
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides three types of extensions - reporting, files uploading and PKGBUILD synchronization. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and should implement at least one of the abstract methods:
The package provides ability to write custom extensions which will be run on (the most) actions, e.g. after updates. By default ahriman provides several types of extensions - reporting, files uploading, PKGBUILD synchronization, repository archiving, housekeeping and distributed builds support. Each extension must derive from the ``ahriman.core.triggers.Trigger`` class and should implement at least one of the abstract methods:
* ``on_result`` - trigger action which will be called after build process, the build result and the list of repository packages will be supplied as arguments.
* ``on_start`` - trigger action which will be called right before the start of the application process.
@@ -14,6 +14,11 @@ Built-in triggers
For the configuration details and settings explanation kindly refer to the :doc:`documentation <configuration>`.
``ahriman.core.archive.ArchiveTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger provides date-based snapshots of the repository. It organizes packages into a daily directory tree (``repos/YYYY/MM/DD``) with its own pacman database. On each run it creates symlinks from the daily snapshot to the actual package archives and maintains the database accordingly. It also takes care of cleaning up broken symlinks and empty directories for packages which have been removed.
``ahriman.core.distributed.WorkerLoaderTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -36,6 +41,16 @@ In order to update those packages you would need to clone your repository separa
This trigger will be called right after build process (``on_result``). It will pick PKGBUILDs for the updated packages, pull them (together with any other files) and commit and push changes to remote repository. No real use cases, but the most of user repositories do it.
``ahriman.core.housekeeping.ArchiveRotationTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This trigger removes old package versions from the archive directory. It implements ``on_result`` and, after each build, compares available versions for updated packages and removes the older ones, keeping only the last N versions as configured by ``keep_built_packages`` option.
``ahriman.core.housekeeping.LogsRotationTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Simple trigger to rotate build logs. It implements ``on_result`` and removes old log records after each build process, keeping only the last N records as configured by ``keep_last_logs`` option.
``ahriman.core.report.ReportTrigger``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@@ -2,13 +2,13 @@
pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.19.0
pkgver=2.20.0rc4
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
url="https://ahriman.readthedocs.io/"
license=('GPL-3.0-or-later')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-bcrypt' 'python-inflection' 'python-pyelftools' 'python-requests')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-bcrypt' 'python-filelock' 'python-inflection' 'python-pyelftools' 'python-requests')
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
source=("https://github.com/arcan1s/ahriman/releases/download/$pkgver/$pkgbase-$pkgver.tar.gz"
"$pkgbase.sysusers"

View File

@@ -23,6 +23,14 @@ sync_files_database = yes
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
use_ahriman_cache = yes
[aur]
; Maximum amount of retries of HTTP requests.
max_retries = 3
; Retry exponential backoff.
retry_backoff = 1.0
; HTTP request timeout in seconds.
;timeout = 30
[build]
; List of additional flags passed to archbuild command.
;archbuild_flags =
@@ -73,8 +81,12 @@ enabled = yes
; In case if unix sockets are used, it might point to the valid socket with encoded path, e.g.:
; address = http+unix://%2Fvar%2Flib%2Fahriman%2Fsocket
;address = http://${web:host}:${web:port}
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; Optional password for authentication (if enabled).
;password =
; Retry exponential backoff.
;retry_backoff = 0.0
; Do not log HTTP errors if occurs.
suppress_http_log_errors = yes
; HTTP request timeout in seconds.
@@ -216,6 +228,10 @@ templates[] = ${prefix}/share/ahriman/templates
;homepage=
; Prefix for packages links. Link to a package will be formed as link_path / filename.
;link_path =
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; Retry exponential backoff.
;retry_backoff = 0.0
; Optional link to the RSS feed.
;rss_url =
; Template name to be used.
@@ -236,12 +252,16 @@ target =
[github]
; Trigger type name.
;type = github
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; GitHub repository owner username.
;owner =
; GitHub API key. public_repo (repo) scope is required.
;password =
; GitHub repository name.
;repository =
; Retry exponential backoff.
;retry_backoff = 0.0
; HTTP request timeout in seconds.
;timeout = 30
; Include repository name to release name (recommended).
@@ -253,6 +273,10 @@ target =
[remote-service]
; Trigger type name.
;type = remote-service
; Maximum amount of retries of HTTP requests.
;max_retries = 0
; Retry exponential backoff.
;retry_backoff = 0.0
; HTTP request timeout in seconds.
;timeout = 30

View File

@@ -87,7 +87,7 @@
};
});
updateTable(table, payload);
updateTable(table, payload, row => row.timestamp);
table.bootstrapTable("hideLoading");
},
onFailure,

View File

@@ -195,16 +195,19 @@
return intervalId;
}
function updateTable(table, rows) {
function updateTable(table, rows, rowChangedKey) {
// instead of using load method here, we just update rows manually to avoid table reinitialization
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
accumulator[row.id] = row["0"];
accumulator[row.id] = {state: row["0"], key: rowChangedKey(row)};
return accumulator;
}, {});
// insert or update rows
// insert or update rows, skipping ones whose status hasn't changed
rows.forEach(row => {
if (Object.hasOwn(currentData, row.id)) {
row["0"] = currentData[row.id]; // copy checkbox state
if (rowChangedKey(row) === currentData[row.id].key) {
return;
}
row["0"] = currentData[row.id].state; // copy checkbox state
table.bootstrapTable("updateByUniqueId", {
id: row.id,
row: row,

View File

@@ -1,6 +1,6 @@
# AUTOMATICALLY GENERATED by `shtab`
_shtab_ahriman_subparsers=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web')
_shtab_ahriman_subparsers=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web' 'web-reload')
_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_add_option_strings=('-h' '--help' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username' '-v' '--variable')
@@ -78,10 +78,11 @@ _shtab_ahriman_user_list_option_strings=('-h' '--help' '-e' '--exit-code' '-R' '
_shtab_ahriman_user_remove_option_strings=('-h' '--help')
_shtab_ahriman_version_option_strings=('-h' '--help')
_shtab_ahriman_web_option_strings=('-h' '--help')
_shtab_ahriman_web_reload_option_strings=('-h' '--help')
_shtab_ahriman_pos_0_choices=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web')
_shtab_ahriman_pos_0_choices=('add' 'aur-search' 'check' 'clean' 'config' 'config-validate' 'copy' 'daemon' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'init' 'key-import' 'package-add' 'package-changes' 'package-changes-remove' 'package-copy' 'package-remove' 'package-status' 'package-status-remove' 'package-status-update' 'package-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'rebuild' 'remove' 'remove-unknown' 'repo-backup' 'repo-check' 'repo-clean' 'repo-config' 'repo-config-validate' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'repo-init' 'repo-rebuild' 'repo-remove-unknown' 'repo-report' 'repo-restore' 'repo-setup' 'repo-sign' 'repo-statistics' 'repo-status-update' 'repo-sync' 'repo-tree' 'repo-triggers' 'repo-update' 'report' 'run' 'search' 'service-clean' 'service-config' 'service-config-validate' 'service-key-import' 'service-repositories' 'service-run' 'service-setup' 'service-shell' 'service-tree-migrate' 'setup' 'shell' 'sign' 'status' 'status-update' 'sync' 'update' 'user-add' 'user-list' 'user-remove' 'version' 'web' 'web-reload')
_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald')
_shtab_ahriman_add__s_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
_shtab_ahriman_add___source_choices=('auto' 'archive' 'aur' 'directory' 'local' 'remote' 'repository')
@@ -572,6 +573,8 @@ _shtab_ahriman_version__h_nargs=0
_shtab_ahriman_version___help_nargs=0
_shtab_ahriman_web__h_nargs=0
_shtab_ahriman_web___help_nargs=0
_shtab_ahriman_web_reload__h_nargs=0
_shtab_ahriman_web_reload___help_nargs=0
# $1=COMP_WORDS[1]
@@ -674,6 +677,7 @@ _shtab_ahriman() {
if [[ "$current_action_nargs" != "*" ]] && \
[[ "$current_action_nargs" != "+" ]] && \
[[ "$current_action_nargs" != "?" ]] && \
[[ "$current_action_nargs" != *"..." ]] && \
(( $word_index + 1 - $current_action_args_start_index - $pos_only >= \
$current_action_nargs )); then

View File

@@ -1,9 +1,9 @@
.TH AHRIMAN "1" "2025\-06\-29" "ahriman 2.19.0" "ArcH linux ReposItory MANager"
.TH AHRIMAN "1" "2026\-02\-21" "ahriman 2.20.0rc4" "ArcH linux ReposItory MANager"
.SH NAME
ahriman \- ArcH linux ReposItory MANager
.SH SYNOPSIS
.B ahriman
[-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] {add,aur-search,check,clean,config,config-validate,copy,daemon,help,help-commands-unsafe,help-updates,help-version,init,key-import,package-add,package-changes,package-changes-remove,package-copy,package-remove,package-status,package-status-remove,package-status-update,package-update,patch-add,patch-list,patch-remove,patch-set-add,rebuild,remove,remove-unknown,repo-backup,repo-check,repo-clean,repo-config,repo-config-validate,repo-create-keyring,repo-create-mirrorlist,repo-daemon,repo-init,repo-rebuild,repo-remove-unknown,repo-report,repo-restore,repo-setup,repo-sign,repo-statistics,repo-status-update,repo-sync,repo-tree,repo-triggers,repo-update,report,run,search,service-clean,service-config,service-config-validate,service-key-import,service-repositories,service-run,service-setup,service-shell,service-tree-migrate,setup,shell,sign,status,status-update,sync,update,user-add,user-list,user-remove,version,web} ...
.B ahriman
[-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] {add,aur-search,check,clean,config,config-validate,copy,daemon,help,help-commands-unsafe,help-updates,help-version,init,key-import,package-add,package-changes,package-changes-remove,package-copy,package-remove,package-status,package-status-remove,package-status-update,package-update,patch-add,patch-list,patch-remove,patch-set-add,rebuild,remove,remove-unknown,repo-backup,repo-check,repo-clean,repo-config,repo-config-validate,repo-create-keyring,repo-create-mirrorlist,repo-daemon,repo-init,repo-rebuild,repo-remove-unknown,repo-report,repo-restore,repo-setup,repo-sign,repo-statistics,repo-status-update,repo-sync,repo-tree,repo-triggers,repo-update,report,run,search,service-clean,service-config,service-config-validate,service-key-import,service-repositories,service-run,service-setup,service-shell,service-tree-migrate,setup,shell,sign,status,status-update,sync,update,user-add,user-list,user-remove,version,web,web-reload} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
@@ -193,11 +193,14 @@ remove user
.TP
\fBahriman\fR \fI\,web\/\fR
web server
.TP
\fBahriman\fR \fI\,web\-reload\/\fR
reload configuration
.SH COMMAND \fI\,'ahriman aur\-search'\/\fR
usage: ahriman aur\-search [\-h] [\-e] [\-\-info | \-\-no\-info]
[\-\-sort\-by {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,repository,submitter,url,url_path,version}]
search [search ...]
usage: ahriman aur\-search [\-h] [\-e] [\-\-info | \-\-no\-info]
[\-\-sort\-by {description,first_submitted,id,last_modified,maintainer,name,num_votes,out_of_date,package_base,package_base_id,popularity,repository,submitter,url,url_path,version}]
search [search ...]
search for package in AUR using API
@@ -220,7 +223,7 @@ sort field by this field. In case if two packages have the same value of the spe
by name
.SH COMMAND \fI\,'ahriman help'\/\fR
usage: ahriman help [\-h] [subcommand]
usage: ahriman help [\-h] [subcommand]
show help message for application or command and exit
@@ -229,7 +232,7 @@ show help message for application or command and exit
show help message for specific command
.SH COMMAND \fI\,'ahriman help\-commands\-unsafe'\/\fR
usage: ahriman help\-commands\-unsafe [\-h] [subcommand ...]
usage: ahriman help\-commands\-unsafe [\-h] [subcommand ...]
list unsafe commands as defined in default args
@@ -239,7 +242,7 @@ instead of showing commands, just test command line for unsafe subcommand and re
otherwise
.SH COMMAND \fI\,'ahriman help\-updates'\/\fR
usage: ahriman help\-updates [\-h] [\-e]
usage: ahriman help\-updates [\-h] [\-e]
request AUR for current version and compare with current service version
@@ -249,15 +252,15 @@ request AUR for current version and compare with current service version
return non\-zero exit code if updates available
.SH COMMAND \fI\,'ahriman help\-version'\/\fR
usage: ahriman help\-version [\-h]
usage: ahriman help\-version [\-h]
print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-\-changes | \-\-no\-changes] [\-\-dependencies | \-\-no\-dependencies] [\-e]
[\-\-increment | \-\-no\-increment] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-v VARIABLE]
package [package ...]
usage: ahriman package\-add [\-h] [\-\-changes | \-\-no\-changes] [\-\-dependencies | \-\-no\-dependencies] [\-e]
[\-\-increment | \-\-no\-increment] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME] [\-v VARIABLE]
package [package ...]
add existing or new package to the build queue
@@ -303,7 +306,7 @@ build as user
apply specified makepkg variables to the next build
.SH COMMAND \fI\,'ahriman package\-changes'\/\fR
usage: ahriman package\-changes [\-h] [\-e] package
usage: ahriman package\-changes [\-h] [\-e] package
retrieve package changes stored in database
@@ -317,7 +320,7 @@ package base
return non\-zero exit status if result is empty
.SH COMMAND \fI\,'ahriman package\-changes\-remove'\/\fR
usage: ahriman package\-changes\-remove [\-h] package
usage: ahriman package\-changes\-remove [\-h] package
remove the package changes stored remotely
@@ -326,7 +329,7 @@ remove the package changes stored remotely
package base
.SH COMMAND \fI\,'ahriman package\-copy'\/\fR
usage: ahriman package\-copy [\-h] [\-e] [\-\-remove] source package [package ...]
usage: ahriman package\-copy [\-h] [\-e] [\-\-remove] source package [package ...]
copy package and its metadata from another repository
@@ -348,7 +351,7 @@ return non\-zero exit status if result is empty
remove package from the source repository after
.SH COMMAND \fI\,'ahriman package\-remove'\/\fR
usage: ahriman package\-remove [\-h] package [package ...]
usage: ahriman package\-remove [\-h] package [package ...]
remove package from the repository
@@ -357,8 +360,8 @@ remove package from the repository
package name or base
.SH COMMAND \fI\,'ahriman package\-status'\/\fR
usage: ahriman package\-status [\-h] [\-\-ahriman] [\-e] [\-\-info | \-\-no\-info] [\-s {unknown,pending,building,failed,success}]
[package ...]
usage: ahriman package\-status [\-h] [\-\-ahriman] [\-e] [\-\-info | \-\-no\-info] [\-s {unknown,pending,building,failed,success}]
[package ...]
request status of the package
@@ -384,7 +387,7 @@ show additional package information
filter packages by status
.SH COMMAND \fI\,'ahriman package\-status\-remove'\/\fR
usage: ahriman package\-status\-remove [\-h] package [package ...]
usage: ahriman package\-status\-remove [\-h] package [package ...]
remove the package from the status page
@@ -393,7 +396,7 @@ remove the package from the status page
remove specified packages from status page
.SH COMMAND \fI\,'ahriman package\-status\-update'\/\fR
usage: ahriman package\-status\-update [\-h] [\-s {unknown,pending,building,failed,success}] [package ...]
usage: ahriman package\-status\-update [\-h] [\-s {unknown,pending,building,failed,success}] [package ...]
update package status on the status page
@@ -407,7 +410,7 @@ set status for specified packages. If no packages supplied, service status will
new package build status
.SH COMMAND \fI\,'ahriman patch\-add'\/\fR
usage: ahriman patch\-add [\-h] package variable [patch]
usage: ahriman patch\-add [\-h] package variable [patch]
create or update patched PKGBUILD function or variable
@@ -424,7 +427,7 @@ PKGBUILD variable or function name. If variable is a function, it must end with
path to file which contains function or variable value. If not set, the value will be read from stdin
.SH COMMAND \fI\,'ahriman patch\-list'\/\fR
usage: ahriman patch\-list [\-h] [\-e] [\-v VARIABLE] package
usage: ahriman patch\-list [\-h] [\-e] [\-v VARIABLE] package
list available patches for the package
@@ -442,7 +445,7 @@ return non\-zero exit status if result is empty
if set, show only patches for specified PKGBUILD variables
.SH COMMAND \fI\,'ahriman patch\-remove'\/\fR
usage: ahriman patch\-remove [\-h] [\-v VARIABLE] package
usage: ahriman patch\-remove [\-h] [\-v VARIABLE] package
remove patches for the package
@@ -457,7 +460,7 @@ should be used for single\-function patches in case if you wold like to remove o
if not set, it will remove all patches related to the package
.SH COMMAND \fI\,'ahriman patch\-set\-add'\/\fR
usage: ahriman patch\-set\-add [\-h] [\-t TRACK] package
usage: ahriman patch\-set\-add [\-h] [\-t TRACK] package
create or update source patches
@@ -471,7 +474,7 @@ path to directory with changed files for patch addition/update
files which has to be tracked
.SH COMMAND \fI\,'ahriman repo\-backup'\/\fR
usage: ahriman repo\-backup [\-h] path
usage: ahriman repo\-backup [\-h] path
backup repository settings and database
@@ -480,9 +483,9 @@ backup repository settings and database
path of the output archive
.SH COMMAND \fI\,'ahriman repo\-check'\/\fR
usage: ahriman repo\-check [\-h] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files] [\-e] [\-\-vcs | \-\-no\-vcs]
[\-y]
[package ...]
usage: ahriman repo\-check [\-h] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files] [\-e] [\-\-vcs | \-\-no\-vcs]
[\-y]
[package ...]
check for packages updates. Same as repo\-update \-\-dry\-run \-\-no\-manual
@@ -512,20 +515,20 @@ fetch actual version of VCS packages
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
.SH COMMAND \fI\,'ahriman repo\-create\-keyring'\/\fR
usage: ahriman repo\-create\-keyring [\-h]
usage: ahriman repo\-create\-keyring [\-h]
create package which contains list of trusted keys as set by configuration. Note, that this action will only create package, the package itself has to be built manually
.SH COMMAND \fI\,'ahriman repo\-create\-mirrorlist'\/\fR
usage: ahriman repo\-create\-mirrorlist [\-h]
usage: ahriman repo\-create\-mirrorlist [\-h]
create package which contains list of available mirrors as set by configuration. Note, that this action will only create package, the package itself has to be built manually
.SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes]
[\-\-check\-files | \-\-no\-check\-files] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run]
[\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual]
[\-\-partitions | \-\-no\-partitions] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes]
[\-\-check\-files | \-\-no\-check\-files] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run]
[\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual]
[\-\-partitions | \-\-no\-partitions] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
start process which periodically will run update process
@@ -583,8 +586,8 @@ fetch actual version of VCS packages
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
.SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-\-increment | \-\-no\-increment]
[\-e] [\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-\-increment | \-\-no\-increment]
[\-e] [\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
force rebuild whole repository
@@ -620,7 +623,7 @@ filter packages by status. Requires \-\-from\-database to be set
build as user
.SH COMMAND \fI\,'ahriman repo\-remove\-unknown'\/\fR
usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run]
usage: ahriman repo\-remove\-unknown [\-h] [\-\-dry\-run]
remove packages which are missing in AUR and do not have local PKGBUILDs
@@ -630,12 +633,12 @@ remove packages which are missing in AUR and do not have local PKGBUILDs
just perform check for packages without removal
.SH COMMAND \fI\,'ahriman repo\-report'\/\fR
usage: ahriman repo\-report [\-h]
usage: ahriman repo\-report [\-h]
generate repository report according to current settings
.SH COMMAND \fI\,'ahriman repo\-restore'\/\fR
usage: ahriman repo\-restore [\-h] [\-o OUTPUT] path
usage: ahriman repo\-restore [\-h] [\-o OUTPUT] path
restore settings and database
@@ -649,7 +652,7 @@ path of the input archive
root path of the extracted files
.SH COMMAND \fI\,'ahriman repo\-sign'\/\fR
usage: ahriman repo\-sign [\-h] [package ...]
usage: ahriman repo\-sign [\-h] [package ...]
(re\-)sign packages and repository database according to current settings
@@ -658,10 +661,10 @@ usage: ahriman repo\-sign [\-h] [package ...]
sign only specified packages
.SH COMMAND \fI\,'ahriman repo\-statistics'\/\fR
usage: ahriman repo\-statistics [\-h] [\-\-chart CHART]
[\-e {package\-outdated,package\-removed,package\-update\-failed,package\-updated}]
[\-\-from\-date FROM_DATE] [\-\-limit LIMIT] [\-\-offset OFFSET] [\-\-to\-date TO_DATE]
[package]
usage: ahriman repo\-statistics [\-h] [\-\-chart CHART]
[\-e {package\-outdated,package\-removed,package\-update\-failed,package\-updated}]
[\-\-from\-date FROM_DATE] [\-\-limit LIMIT] [\-\-offset OFFSET] [\-\-to\-date TO_DATE]
[package]
fetch repository statistics
@@ -695,7 +698,7 @@ skip specified amount of events
only fetch events which are older than the date
.SH COMMAND \fI\,'ahriman repo\-status\-update'\/\fR
usage: ahriman repo\-status\-update [\-h] [\-s {unknown,pending,building,failed,success}]
usage: ahriman repo\-status\-update [\-h] [\-s {unknown,pending,building,failed,success}]
update repository status on the status page
@@ -705,12 +708,12 @@ update repository status on the status page
new status
.SH COMMAND \fI\,'ahriman repo\-sync'\/\fR
usage: ahriman repo\-sync [\-h]
usage: ahriman repo\-sync [\-h]
sync repository files to remote server according to current settings
.SH COMMAND \fI\,'ahriman repo\-tree'\/\fR
usage: ahriman repo\-tree [\-h] [\-p PARTITIONS]
usage: ahriman repo\-tree [\-h] [\-p PARTITIONS]
dump repository tree based on packages dependencies
@@ -720,7 +723,7 @@ dump repository tree based on packages dependencies
also divide packages by independent partitions
.SH COMMAND \fI\,'ahriman repo\-triggers'\/\fR
usage: ahriman repo\-triggers [\-h] [trigger ...]
usage: ahriman repo\-triggers [\-h] [trigger ...]
run triggers on empty build result as configured by settings
@@ -729,10 +732,10 @@ run triggers on empty build result as configured by settings
instead of running all triggers as set by configuration, just process specified ones in order of mention
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files]
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[package ...]
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files]
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[package ...]
check for packages updates and run build process if requested
@@ -790,8 +793,8 @@ fetch actual version of VCS packages
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
.SH COMMAND \fI\,'ahriman service\-clean'\/\fR
usage: ahriman service\-clean [\-h] [\-\-cache | \-\-no\-cache] [\-\-chroot | \-\-no\-chroot] [\-\-manual | \-\-no\-manual]
[\-\-packages | \-\-no\-packages] [\-\-pacman | \-\-no\-pacman]
usage: ahriman service\-clean [\-h] [\-\-cache | \-\-no\-cache] [\-\-chroot | \-\-no\-chroot] [\-\-manual | \-\-no\-manual]
[\-\-packages | \-\-no\-packages] [\-\-pacman | \-\-no\-pacman]
remove local caches
@@ -817,7 +820,7 @@ clear directory with built packages
clear directory with pacman local database cache
.SH COMMAND \fI\,'ahriman service\-config'\/\fR
usage: ahriman service\-config [\-h] [\-\-info | \-\-no\-info] [\-\-secure | \-\-no\-secure] [section] [key]
usage: ahriman service\-config [\-h] [\-\-info | \-\-no\-info] [\-\-secure | \-\-no\-secure] [section] [key]
dump configuration for the specified architecture
@@ -839,7 +842,7 @@ show additional information, e.g. configuration files
hide passwords and secrets from output
.SH COMMAND \fI\,'ahriman service\-config\-validate'\/\fR
usage: ahriman service\-config\-validate [\-h] [\-e]
usage: ahriman service\-config\-validate [\-h] [\-e]
validate configuration and print found errors
@@ -849,7 +852,7 @@ validate configuration and print found errors
return non\-zero exit status if configuration is invalid
.SH COMMAND \fI\,'ahriman service\-key\-import'\/\fR
usage: ahriman service\-key\-import [\-h] [\-\-key\-server KEY_SERVER] key
usage: ahriman service\-key\-import [\-h] [\-\-key\-server KEY_SERVER] key
import PGP key from public sources to the repository user
@@ -863,7 +866,7 @@ PGP key to import from public server
key server for key import
.SH COMMAND \fI\,'ahriman service\-repositories'\/\fR
usage: ahriman service\-repositories [\-h] [\-\-id\-only | \-\-no\-id\-only]
usage: ahriman service\-repositories [\-h] [\-\-id\-only | \-\-no\-id\-only]
list all available repositories
@@ -873,7 +876,7 @@ list all available repositories
show machine readable identifier instead
.SH COMMAND \fI\,'ahriman service\-run'\/\fR
usage: ahriman service\-run [\-h] command [command ...]
usage: ahriman service\-run [\-h] command [command ...]
run multiple commands on success run of the previous command
@@ -882,11 +885,11 @@ run multiple commands on success run of the previous command
command to be run (quoted) without ``ahriman``
.SH COMMAND \fI\,'ahriman service\-setup'\/\fR
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION]
[\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]
[\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib] \-\-packager PACKAGER [\-\-server SERVER]
[\-\-sign\-key SIGN_KEY] [\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT]
[\-\-web\-unix\-socket WEB_UNIX_SOCKET]
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION]
[\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]
[\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib] \-\-packager PACKAGER [\-\-server SERVER]
[\-\-sign\-key SIGN_KEY] [\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT]
[\-\-web\-unix\-socket WEB_UNIX_SOCKET]
create initial service configuration, requires root
@@ -940,7 +943,7 @@ port of the web service
path to unix socket used for interprocess communications
.SH COMMAND \fI\,'ahriman service\-shell'\/\fR
usage: ahriman service\-shell [\-h] [\-o OUTPUT] [code]
usage: ahriman service\-shell [\-h] [\-o OUTPUT] [code]
drop into python shell
@@ -954,13 +957,13 @@ instead of dropping into shell, just execute the specified code
output commands and result to the file
.SH COMMAND \fI\,'ahriman service\-tree\-migrate'\/\fR
usage: ahriman service\-tree\-migrate [\-h]
usage: ahriman service\-tree\-migrate [\-h]
migrate repository tree between versions
.SH COMMAND \fI\,'ahriman user\-add'\/\fR
usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-R {unauthorized,read,reporter,full}]
username
usage: ahriman user\-add [\-h] [\-\-key KEY] [\-\-packager PACKAGER] [\-p PASSWORD] [\-R {unauthorized,read,reporter,full}]
username
update user for web services with the given password and role. In case if password was not entered it will be asked interactively
@@ -987,7 +990,7 @@ authorization type.
user access level
.SH COMMAND \fI\,'ahriman user\-list'\/\fR
usage: ahriman user\-list [\-h] [\-e] [\-R {unauthorized,read,reporter,full}] [username]
usage: ahriman user\-list [\-h] [\-e] [\-R {unauthorized,read,reporter,full}] [username]
list users from the user mapping and their roles
@@ -1005,7 +1008,7 @@ return non\-zero exit status if result is empty
filter users by role
.SH COMMAND \fI\,'ahriman user\-remove'\/\fR
usage: ahriman user\-remove [\-h] username
usage: ahriman user\-remove [\-h] username
remove user from the user mapping and update the configuration
@@ -1014,10 +1017,15 @@ remove user from the user mapping and update the configuration
username for web service
.SH COMMAND \fI\,'ahriman web'\/\fR
usage: ahriman web [\-h]
usage: ahriman web [\-h]
start web server
.SH COMMAND \fI\,'ahriman web\-reload'\/\fR
usage: ahriman web\-reload [\-h]
reload web server configuration
.SH COMMENTS
Quick setup command (replace repository name, architecture and packager as needed):

View File

@@ -80,6 +80,7 @@ _shtab_ahriman_commands() {
"user-remove:remove user from the user mapping and update the configuration"
"version:print application and its dependencies versions"
"web:start web server"
"web-reload:reload web server configuration"
)
_describe 'ahriman commands' _commands
}
@@ -99,6 +100,9 @@ _shtab_ahriman_options=(
"--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:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_defaults_added=0
_shtab_ahriman_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
@@ -113,6 +117,9 @@ _shtab_ahriman_add_options=(
"(*):package source (base name, path to local files, remote URL):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_add_defaults_added=0
_shtab_ahriman_aur_search_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@@ -121,6 +128,9 @@ _shtab_ahriman_aur_search_options=(
"(*):search terms, can be specified multiple times, the result will match all terms:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_aur_search_defaults_added=0
_shtab_ahriman_check_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
@@ -131,6 +141,9 @@ _shtab_ahriman_check_options=(
"(*)::filter check by package base (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_check_defaults_added=0
_shtab_ahriman_clean_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--cache,--no-cache}"[clear directory with package caches (default\: False)]:cache:"
@@ -140,6 +153,9 @@ _shtab_ahriman_clean_options=(
{--pacman,--no-pacman}"[clear directory with pacman local database cache (default\: False)]:pacman:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_clean_defaults_added=0
_shtab_ahriman_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--info,--no-info}"[show additional information, e.g. configuration files (default\: True)]:info:"
@@ -148,11 +164,17 @@ _shtab_ahriman_config_options=(
":filter settings by key (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_config_defaults_added=0
_shtab_ahriman_config_validate_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if configuration is invalid (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_config_validate_defaults_added=0
_shtab_ahriman_copy_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@@ -161,6 +183,9 @@ _shtab_ahriman_copy_options=(
"(*):package base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_copy_defaults_added=0
_shtab_ahriman_daemon_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
@@ -178,25 +203,40 @@ _shtab_ahriman_daemon_options=(
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_daemon_defaults_added=0
_shtab_ahriman_help_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
":show help message for specific command (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_help_defaults_added=0
_shtab_ahriman_help_commands_unsafe_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*)::instead of showing commands, just test command line for unsafe subcommand and return 0 in case if command is safe and 1 otherwise (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_help_commands_unsafe_defaults_added=0
_shtab_ahriman_help_updates_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit code if updates available (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_help_updates_defaults_added=0
_shtab_ahriman_help_version_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_help_version_defaults_added=0
_shtab_ahriman_init_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
@@ -213,12 +253,18 @@ _shtab_ahriman_init_options=(
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_init_defaults_added=0
_shtab_ahriman_key_import_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--key-server[key server for key import (default\: keyserver.ubuntu.com)]:key_server:"
":PGP key to import from public server:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_key_import_defaults_added=0
_shtab_ahriman_package_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
@@ -233,17 +279,26 @@ _shtab_ahriman_package_add_options=(
"(*):package source (base name, path to local files, remote URL):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_add_defaults_added=0
_shtab_ahriman_package_changes_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
":package base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_changes_defaults_added=0
_shtab_ahriman_package_changes_remove_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
":package base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_changes_remove_defaults_added=0
_shtab_ahriman_package_copy_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@@ -252,11 +307,17 @@ _shtab_ahriman_package_copy_options=(
"(*):package base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_copy_defaults_added=0
_shtab_ahriman_package_remove_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):package name or base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_remove_defaults_added=0
_shtab_ahriman_package_status_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--ahriman[get service status itself (default\: False)]"
@@ -266,17 +327,26 @@ _shtab_ahriman_package_status_options=(
"(*)::filter status by package base (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_status_defaults_added=0
_shtab_ahriman_package_status_remove_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):remove specified packages from status page:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_status_remove_defaults_added=0
_shtab_ahriman_package_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-s,--status}"[new package build status (default\: success)]:status:(unknown pending building failed success)"
"(*)::set status for specified packages. If no packages supplied, service status will be updated (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_status_update_defaults_added=0
_shtab_ahriman_package_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
@@ -291,6 +361,9 @@ _shtab_ahriman_package_update_options=(
"(*):package source (base name, path to local files, remote URL):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_package_update_defaults_added=0
_shtab_ahriman_patch_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
":package base:"
@@ -298,6 +371,9 @@ _shtab_ahriman_patch_add_options=(
":path to file which contains function or variable value. If not set, the value will be read from stdin (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_patch_add_defaults_added=0
_shtab_ahriman_patch_list_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@@ -305,18 +381,27 @@ _shtab_ahriman_patch_list_options=(
":package base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_patch_list_defaults_added=0
_shtab_ahriman_patch_remove_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"*"{-v,--variable}"[should be used for single-function patches in case if you wold like to remove only specified PKGBUILD variables. In case if not set, it will remove all patches related to the package (default\: None)]:variable:"
":package base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_patch_remove_defaults_added=0
_shtab_ahriman_patch_set_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"*"{-t,--track}"[files which has to be tracked (default\: \[\'\*.diff\', \'\*.patch\'\])]:track:"
":path to directory with changed files for patch addition\/update:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_patch_set_add_defaults_added=0
_shtab_ahriman_rebuild_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"*--depends-on[only rebuild packages that depend on specified packages (default\: None)]:depends_on:"
@@ -328,21 +413,33 @@ _shtab_ahriman_rebuild_options=(
{-u,--username}"[build as user (default\: None)]:username:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_rebuild_defaults_added=0
_shtab_ahriman_remove_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):package name or base:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_remove_defaults_added=0
_shtab_ahriman_remove_unknown_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--dry-run[just perform check for packages without removal (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_remove_unknown_defaults_added=0
_shtab_ahriman_repo_backup_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
":path of the output archive:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_backup_defaults_added=0
_shtab_ahriman_repo_check_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--changes,--no-changes}"[calculate changes from the latest known commit if available (default\: True)]:changes:"
@@ -353,6 +450,9 @@ _shtab_ahriman_repo_check_options=(
"(*)::filter check by package base (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_check_defaults_added=0
_shtab_ahriman_repo_clean_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--cache,--no-cache}"[clear directory with package caches (default\: False)]:cache:"
@@ -362,6 +462,9 @@ _shtab_ahriman_repo_clean_options=(
{--pacman,--no-pacman}"[clear directory with pacman local database cache (default\: False)]:pacman:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_clean_defaults_added=0
_shtab_ahriman_repo_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--info,--no-info}"[show additional information, e.g. configuration files (default\: True)]:info:"
@@ -370,19 +473,31 @@ _shtab_ahriman_repo_config_options=(
":filter settings by key (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_config_defaults_added=0
_shtab_ahriman_repo_config_validate_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if configuration is invalid (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_config_validate_defaults_added=0
_shtab_ahriman_repo_create_keyring_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_create_keyring_defaults_added=0
_shtab_ahriman_repo_create_mirrorlist_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_create_mirrorlist_defaults_added=0
_shtab_ahriman_repo_daemon_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
@@ -400,6 +515,9 @@ _shtab_ahriman_repo_daemon_options=(
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_daemon_defaults_added=0
_shtab_ahriman_repo_init_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
@@ -416,6 +534,9 @@ _shtab_ahriman_repo_init_options=(
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_init_defaults_added=0
_shtab_ahriman_repo_rebuild_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"*--depends-on[only rebuild packages that depend on specified packages (default\: None)]:depends_on:"
@@ -427,21 +548,33 @@ _shtab_ahriman_repo_rebuild_options=(
{-u,--username}"[build as user (default\: None)]:username:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_rebuild_defaults_added=0
_shtab_ahriman_repo_remove_unknown_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--dry-run[just perform check for packages without removal (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_remove_unknown_defaults_added=0
_shtab_ahriman_repo_report_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_report_defaults_added=0
_shtab_ahriman_repo_restore_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-o,--output}"[root path of the extracted files (default\: \/)]:output:"
":path of the input archive:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_restore_defaults_added=0
_shtab_ahriman_repo_setup_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
@@ -458,11 +591,17 @@ _shtab_ahriman_repo_setup_options=(
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_setup_defaults_added=0
_shtab_ahriman_repo_sign_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*)::sign only specified packages (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_sign_defaults_added=0
_shtab_ahriman_repo_statistics_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--chart[create updates chart and save it to the specified path (default\: None)]:chart:"
@@ -474,25 +613,40 @@ _shtab_ahriman_repo_statistics_options=(
":fetch only events for the specified package (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_statistics_defaults_added=0
_shtab_ahriman_repo_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-s,--status}"[new status (default\: success)]:status:(unknown pending building failed success)"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_status_update_defaults_added=0
_shtab_ahriman_repo_sync_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_sync_defaults_added=0
_shtab_ahriman_repo_tree_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-p,--partitions}"[also divide packages by independent partitions (default\: 1)]:partitions:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_tree_defaults_added=0
_shtab_ahriman_repo_triggers_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*)::instead of running all triggers as set by configuration, just process specified ones in order of mention (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_triggers_defaults_added=0
_shtab_ahriman_repo_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
@@ -510,15 +664,24 @@ _shtab_ahriman_repo_update_options=(
"(*)::filter check by package base (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_repo_update_defaults_added=0
_shtab_ahriman_report_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_report_defaults_added=0
_shtab_ahriman_run_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_run_defaults_added=0
_shtab_ahriman_search_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@@ -527,6 +690,9 @@ _shtab_ahriman_search_options=(
"(*):search terms, can be specified multiple times, the result will match all terms:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_search_defaults_added=0
_shtab_ahriman_service_clean_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--cache,--no-cache}"[clear directory with package caches (default\: False)]:cache:"
@@ -536,6 +702,9 @@ _shtab_ahriman_service_clean_options=(
{--pacman,--no-pacman}"[clear directory with pacman local database cache (default\: False)]:pacman:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_clean_defaults_added=0
_shtab_ahriman_service_config_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--info,--no-info}"[show additional information, e.g. configuration files (default\: True)]:info:"
@@ -544,27 +713,42 @@ _shtab_ahriman_service_config_options=(
":filter settings by key (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_config_defaults_added=0
_shtab_ahriman_service_config_validate_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if configuration is invalid (default\: False)]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_config_validate_defaults_added=0
_shtab_ahriman_service_key_import_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--key-server[key server for key import (default\: keyserver.ubuntu.com)]:key_server:"
":PGP key to import from public server:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_key_import_defaults_added=0
_shtab_ahriman_service_repositories_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--id-only,--no-id-only}"[show machine readable identifier instead (default\: False)]:id_only:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_repositories_defaults_added=0
_shtab_ahriman_service_run_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*):command to be run (quoted) without \`\`ahriman\`\`:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_run_defaults_added=0
_shtab_ahriman_service_setup_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
@@ -581,16 +765,25 @@ _shtab_ahriman_service_setup_options=(
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_setup_defaults_added=0
_shtab_ahriman_service_shell_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-o,--output}"[output commands and result to the file (default\: None)]:output:"
":instead of dropping into shell, just execute the specified code (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_shell_defaults_added=0
_shtab_ahriman_service_tree_migrate_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_service_tree_migrate_defaults_added=0
_shtab_ahriman_setup_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--build-as-user[force makepkg user to the specific one (default\: None)]:build_as_user:"
@@ -607,17 +800,26 @@ _shtab_ahriman_setup_options=(
"--web-unix-socket[path to unix socket used for interprocess communications (default\: None)]:web_unix_socket:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_setup_defaults_added=0
_shtab_ahriman_shell_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-o,--output}"[output commands and result to the file (default\: None)]:output:"
":instead of dropping into shell, just execute the specified code (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_shell_defaults_added=0
_shtab_ahriman_sign_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"(*)::sign only specified packages (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_sign_defaults_added=0
_shtab_ahriman_status_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--ahriman[get service status itself (default\: False)]"
@@ -627,16 +829,25 @@ _shtab_ahriman_status_options=(
"(*)::filter status by package base (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_status_defaults_added=0
_shtab_ahriman_status_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-s,--status}"[new package build status (default\: success)]:status:(unknown pending building failed success)"
"(*)::set status for specified packages. If no packages supplied, service status will be updated (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_status_update_defaults_added=0
_shtab_ahriman_sync_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_sync_defaults_added=0
_shtab_ahriman_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
@@ -654,6 +865,9 @@ _shtab_ahriman_update_options=(
"(*)::filter check by package base (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_update_defaults_added=0
_shtab_ahriman_user_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"--key[optional PGP key used by this user. The private key must be imported (default\: None)]:key:"
@@ -663,6 +877,9 @@ _shtab_ahriman_user_add_options=(
":username for web service:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_user_add_defaults_added=0
_shtab_ahriman_user_list_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
@@ -670,25 +887,48 @@ _shtab_ahriman_user_list_options=(
":filter users by username (default\: None):"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_user_list_defaults_added=0
_shtab_ahriman_user_remove_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
":username for web service:"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_user_remove_defaults_added=0
_shtab_ahriman_version_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_version_defaults_added=0
_shtab_ahriman_web_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_web_defaults_added=0
_shtab_ahriman_web_reload_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
)
# guard to ensure default positional specs are added only once per session
_shtab_ahriman_web_reload_defaults_added=0
_shtab_ahriman() {
local context state line curcontext="$curcontext" one_or_more='(-)*' remainder='(*)'
local context state line curcontext="$curcontext" one_or_more='(*)' remainder='(-)*' default='*::: :->ahriman'
if ((${_shtab_ahriman_options[(I)${(q)one_or_more}*]} + ${_shtab_ahriman_options[(I)${(q)remainder}*]} == 0)); then # noqa: E501
_shtab_ahriman_options+=(': :_shtab_ahriman_commands' '*::: :->ahriman')
# Add default positional/remainder specs only if none exist, and only once per session
if (( ! _shtab_ahriman_defaults_added )); then
if (( ${_shtab_ahriman_options[(I)${(q)one_or_more}*]} + ${_shtab_ahriman_options[(I)${(q)remainder}*]} + ${_shtab_ahriman_options[(I)${(q)default}]} == 0 )); then
_shtab_ahriman_options+=(': :_shtab_ahriman_commands' '*::: :->ahriman')
fi
_shtab_ahriman_defaults_added=1
fi
_arguments -C -s $_shtab_ahriman_options
@@ -773,6 +1013,7 @@ _shtab_ahriman() {
user-remove) _arguments -C -s $_shtab_ahriman_user_remove_options ;;
version) _arguments -C -s $_shtab_ahriman_version_options ;;
web) _arguments -C -s $_shtab_ahriman_web_options ;;
web-reload) _arguments -C -s $_shtab_ahriman_web_reload_options ;;
esac
esac
}

View File

@@ -18,6 +18,7 @@ authors = [
dependencies = [
"bcrypt",
"filelock",
"inflection",
"pyelftools",
"requests",

View File

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

View File

@@ -22,6 +22,7 @@ from collections.abc import Iterable
from ahriman.application.application.application_properties import ApplicationProperties
from ahriman.application.application.workers import Updater
from ahriman.core.build_tools.sources import Sources
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.package import Package
from ahriman.models.packagers import Packagers
from ahriman.models.result import Result
@@ -116,7 +117,7 @@ class ApplicationRepository(ApplicationProperties):
for single in probe.packages:
try:
_ = Package.from_aur(single, None)
except Exception:
except UnknownPackageError:
packages.append(single)
return packages

View File

@@ -81,11 +81,13 @@ class Backup(Handler):
Returns:
set[Path]: map of the filesystem paths
"""
paths = set(configuration.include.glob("*.ini"))
# configuration files
root, _ = configuration.check_loaded()
paths.add(root) # the configuration itself
paths.add(SQLite.database_path(configuration)) # database
paths = set(configuration.includes)
paths.add(root)
# database
paths.add(SQLite.database_path(configuration))
# local caches
repository_paths = configuration.repository_paths

View File

@@ -20,7 +20,7 @@
import argparse
import logging
from collections.abc import Callable, Iterable
from collections.abc import Callable
from multiprocessing import Pool
from typing import ClassVar, TypeVar
@@ -28,9 +28,9 @@ from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import ExitCode, MissingArchitectureError, MultipleArchitecturesError
from ahriman.core.log.log_loader import LogLoader
from ahriman.core.repository import Explorer
from ahriman.core.types import ExplicitBool
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
# this workaround is for several things
@@ -169,11 +169,6 @@ class Handler:
Raises:
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
configuration = Configuration()
configuration.load(args.configuration)
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# preparse systemd repository-id argument
# we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes
if args.repository_id is not None:
@@ -184,27 +179,10 @@ class Handler:
if repository_parts:
args.repository = "-".join(repository_parts) # replace slash with dash
# extract repository names first
if (from_args := args.repository) is not None:
repositories: Iterable[str] = [from_args]
elif from_filesystem := RepositoryPaths.known_repositories(root):
repositories = from_filesystem
else: # try to read configuration now
repositories = [configuration.get("repository", "name")]
configuration = Configuration()
configuration.load(args.configuration)
repositories = Explorer.repositories_extract(configuration, args.repository, args.architecture)
# extract architecture names
if (architecture := args.architecture) is not None:
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
)
else: # try to read from file system
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
for architecture in RepositoryPaths.known_architectures(root, repository)
)
if not parsed:
if not repositories:
raise MissingArchitectureError(args.command)
return sorted(parsed)
return sorted(repositories)

View File

@@ -47,7 +47,7 @@ class Restore(Handler):
report(bool): force enable or disable reporting
"""
with tarfile.open(args.path) as archive:
archive.extractall(path=args.output) # nosec
archive.extractall(path=args.output, filter="data")
@staticmethod
def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:

View File

@@ -72,14 +72,16 @@ class Setup(Handler):
application = Application(repository_id, configuration, report=report)
with application.repository.paths.preserve_owner():
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
# basically we create configuration here as root, but it is ok, because those files are only used for reading
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
# finish initialization
with application.repository.paths.preserve_owner():
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
@@ -249,7 +251,7 @@ class Setup(Handler):
content = f"PACKAGER='{packager}'\n"
if makeflags_jobs:
content += """MAKEFLAGS="-j$(nproc)"\n"""
content += "MAKEFLAGS=\"-j$(nproc)\"\n"
uid, _ = paths.root_owner
home_dir = Path(getpwuid(uid).pw_dir)

View File

@@ -21,7 +21,7 @@ import argparse
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.utils import walk
from ahriman.core.utils import symlink_relative, walk
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
@@ -50,7 +50,7 @@ class TreeMigrate(Handler):
target_tree.tree_create()
# perform migration
TreeMigrate.tree_move(current_tree, target_tree)
TreeMigrate.fix_symlinks(target_tree)
TreeMigrate.symlinks_fix(target_tree)
@staticmethod
def _set_service_tree_migrate_parser(root: SubParserAction) -> argparse.ArgumentParser:
@@ -69,9 +69,9 @@ class TreeMigrate(Handler):
return parser
@staticmethod
def fix_symlinks(paths: RepositoryPaths) -> None:
def symlinks_fix(paths: RepositoryPaths) -> None:
"""
fix packages archives symlinks
fix package archive symlinks
Args:
paths(RepositoryPaths): new repository paths
@@ -82,7 +82,7 @@ class TreeMigrate(Handler):
continue
if (source_archive := archives.get(symlink.name)) is not None:
symlink.unlink()
symlink.symlink_to(source_archive.relative_to(symlink.parent, walk_up=True))
symlink_relative(symlink, source_archive)
@staticmethod
def tree_move(from_tree: RepositoryPaths, to_tree: RepositoryPaths) -> None:

View File

@@ -21,7 +21,7 @@ import argparse
import re
import sys
from collections.abc import Generator
from collections.abc import Iterator
from importlib import metadata
from typing import ClassVar
@@ -77,7 +77,7 @@ class Versions(Handler):
return parser
@staticmethod
def package_dependencies(root: str) -> Generator[tuple[str, str], None, None]:
def package_dependencies(root: str) -> Iterator[tuple[str, str]]:
"""
extract list of ahriman package dependencies installed into system with their versions
@@ -87,7 +87,7 @@ class Versions(Handler):
Yields:
tuple[str, str]: map of installed dependency to its version
"""
def dependencies_by_key(key: str) -> Generator[str, None, None]:
def dependencies_by_key(key: str) -> Iterator[str]:
# in importlib it returns requires in the following format
# ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"]
try:

View File

@@ -19,7 +19,7 @@
#
import argparse
from collections.abc import Generator
from collections.abc import Iterator
from pathlib import Path
from ahriman.application.handlers.handler import Handler, SubParserAction
@@ -86,7 +86,7 @@ class Web(Handler):
return parser
@staticmethod
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Generator[str, None, None]:
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Iterator[str]:
"""
extract list of arguments used for current command, except for command specific ones

View File

@@ -158,7 +158,7 @@ class Lock(LazyLogging):
"""
check if current user is actually owner of ahriman root
"""
check_user(self.paths, unsafe=self.unsafe)
check_user(self.paths.root, unsafe=self.unsafe)
self.paths.tree_create()
def check_version(self) -> None:

View File

@@ -21,7 +21,7 @@ import itertools
import shutil
import tarfile
from collections.abc import Generator, Iterable
from collections.abc import Iterable, Iterator
from functools import cached_property
from pathlib import Path
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
@@ -130,7 +130,7 @@ class Pacman(LazyLogging):
return # database for some reason deos not exist
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
with self.repository_paths.preserve_owner(dst.parent):
with self.repository_paths.preserve_owner():
shutil.copy(src, dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
@@ -188,7 +188,7 @@ class Pacman(LazyLogging):
Returns:
dict[str, set[str]]: map of package name to its list of files
"""
def extract(tar: tarfile.TarFile, versions: dict[str, str]) -> Generator[tuple[str, set[str]], None, None]:
def extract(tar: tarfile.TarFile, versions: dict[str, str]) -> Iterator[tuple[str, set[str]]]:
for package_name, version in versions.items():
path = Path(f"{package_name}-{version}") / "files"
try:
@@ -223,7 +223,7 @@ class Pacman(LazyLogging):
return result
def package(self, package_name: str) -> Generator[Package, None, None]:
def package(self, package_name: str) -> Iterator[Package]:
"""
retrieve list of the packages from the repository by name
@@ -256,7 +256,7 @@ class Pacman(LazyLogging):
return result
def provided_by(self, package_name: str) -> Generator[Package, None, None]:
def provided_by(self, package_name: str) -> Iterator[Package]:
"""
search through databases and emit packages which provides the ``package_name``

View File

@@ -21,7 +21,7 @@ import itertools
import re
import shlex
from collections.abc import Generator
from collections.abc import Iterator
from enum import StrEnum
from typing import IO
@@ -209,7 +209,7 @@ class PkgbuildParser(shlex.shlex):
Raises:
PkgbuildParserError: if array is not closed
"""
def extract() -> Generator[str, None, None]:
def extract() -> Iterator[str]:
while token := self.get_token():
match token:
case _ if self._is_escaped():
@@ -276,7 +276,7 @@ class PkgbuildParser(shlex.shlex):
return content
def _parse_token(self, token: str) -> Generator[PkgbuildPatch, None, None]:
def _parse_token(self, token: str) -> Iterator[PkgbuildPatch]:
"""
parse single token to the PKGBUILD field
@@ -360,7 +360,7 @@ class PkgbuildParser(shlex.shlex):
raise PkgbuildParserError("reached starting position, no valid symbols found")
def parse(self) -> Generator[PkgbuildPatch, None, None]:
def parse(self) -> Iterator[PkgbuildPatch]:
"""
parse source stream and yield parsed entries

View File

@@ -59,22 +59,15 @@ class Repo(LazyLogging):
"""
return self.root / f"{self.name}.db.tar.gz"
def add(self, path: Path, *, remove: bool = True) -> None:
def add(self, path: Path) -> None:
"""
add new package to repository
Args:
path(Path): path to archive to add
remove(bool, optional): whether to remove old packages or not (Default value = True)
"""
command = ["repo-add", *self.sign_args]
if remove:
command.extend(["--remove"])
command.extend([str(self.repo_path), str(path)])
# add to repository
check_output(
*command,
"repo-add", *self.sign_args, "--remove", str(self.repo_path), str(path),
exception=BuildError.from_process(path.name),
cwd=self.root,
logger=self.logger,
@@ -88,18 +81,16 @@ class Repo(LazyLogging):
check_output("repo-add", *self.sign_args, str(self.repo_path),
cwd=self.root, logger=self.logger, user=self.uid)
def remove(self, package_name: str | None, filename: Path) -> None:
def remove(self, package_name: str, filename: Path) -> None:
"""
remove package from repository
Args:
package_name(str | None): package name to remove. If none set, it will be guessed from filename
package_name(str): package name to remove
filename(Path): package filename to remove
"""
package_name = package_name or filename.name.rsplit("-", maxsplit=3)[0]
# remove package and signature (if any) from filesystem
for full_path in self.root.glob(f"**/{filename.name}*"):
for full_path in self.root.glob(f"{filename.name}*"):
full_path.unlink()
# remove package from registry

View File

@@ -19,12 +19,14 @@
#
import datetime
from collections.abc import Iterator
from pathlib import Path
from ahriman.core.alpm.repo import Repo
from ahriman.core.log import LazyLogging
from ahriman.core.utils import utcnow, walk
from ahriman.core.utils import package_like, symlink_relative, utcnow, walk
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.repository_paths import RepositoryPaths
@@ -48,6 +50,58 @@ class ArchiveTree(LazyLogging):
self.repository_id = repository_path.repository_id
self.sign_args = sign_args
@staticmethod
def _package_symlinks_create(package_description: PackageDescription, root: Path, archive: Path) -> bool:
"""
process symlinks creation for single package
Args:
package_description(PackageDescription): archive descriptor
root(Path): path to the archive repository root
archive(Path): path to directory with archives
Returns:
bool: ``True`` if symlinks were created and ``False`` otherwise
"""
symlinks_created = False
# here we glob for archive itself and signature if any
for file in archive.glob(f"{package_description.filename}*"):
try:
symlink_relative(root / file.name, file)
symlinks_created = True
except FileExistsError:
continue # symlink is already created, skip processing
return symlinks_created
def _repo(self, root: Path) -> Repo:
"""
constructs :class:`ahriman.core.alpm.repo.Repo` object for given path
Args:
root(Path): root of the repository
Returns:
Repo: constructed object with correct properties
"""
return Repo(self.repository_id.name, self.paths, self.sign_args, root)
def directories_fix(self, paths: set[Path]) -> None:
"""
remove empty repository directories recursively
Args:
paths(set[Path]): repositories to check
"""
root = self.paths.archive / "repos"
for repository in paths:
parents = [repository] + list(repository.parents[:-1])
for parent in parents:
path = root / parent
if list(path.iterdir()):
continue # directory is not empty
path.rmdir()
def repository_for(self, date: datetime.date | None = None) -> Path:
"""
get full path to repository at the specified date
@@ -78,7 +132,7 @@ class ArchiveTree(LazyLogging):
packages(list[Package]): list of packages to be updated
"""
root = self.repository_for()
repo = Repo(self.repository_id.name, self.paths, self.sign_args, root)
repo = self._repo(root)
for package in packages:
archive = self.paths.archive_for(package.base)
@@ -88,21 +142,15 @@ class ArchiveTree(LazyLogging):
self.logger.warning("received empty package filename for %s", package_name)
continue
has_file = False
for file in archive.glob(f"{single.filename}*"):
symlink = root / file.name
try:
symlink.symlink_to(file.relative_to(symlink.parent, walk_up=True))
has_file = True
except FileExistsError:
continue # symlink is already created, skip processing
if has_file:
if self._package_symlinks_create(single, root, archive):
repo.add(root / single.filename)
def symlinks_fix(self) -> None:
def symlinks_fix(self) -> Iterator[Path]:
"""
remove broken symlinks across repositories for all dates
Yields:
Path: path of the sub-repository with removed symlinks
"""
for path in walk(self.paths.archive / "repos"):
root = path.parent
@@ -110,12 +158,18 @@ class ArchiveTree(LazyLogging):
if self.repository_id.name != name or self.repository_id.architecture != architecture:
continue # we only process same name repositories
if not package_like(path):
continue
if not path.is_symlink():
continue # find symlinks only
if path.exists():
continue # filter out not broken symlinks
Repo(self.repository_id.name, self.paths, self.sign_args, root).remove(None, path)
# here we don't have access to original archive, so we have to guess name based on archive name
# normally it should be fine to do so
package_name = path.name.rsplit("-", maxsplit=3)[0]
self._repo(root).remove(package_name, path)
yield path.parent.relative_to(self.paths.archive / "repos")
def tree_create(self) -> None:
"""
@@ -125,7 +179,7 @@ class ArchiveTree(LazyLogging):
if root.exists():
return
with self.paths.preserve_owner(self.paths.archive):
with self.paths.preserve_owner():
root.mkdir(0o755, parents=True)
# init empty repository here
Repo(self.repository_id.name, self.paths, self.sign_args, root).init()
self._repo(root).init()

View File

@@ -66,4 +66,5 @@ class ArchiveTrigger(Trigger):
"""
trigger action which will be called before the stop of the application
"""
self.tree.symlinks_fix()
repositories = set(self.tree.symlinks_fix())
self.tree.directories_fix(repositories)

View File

@@ -22,6 +22,11 @@ try:
except ImportError:
aiohttp_security = None # type: ignore[assignment]
try:
import aiohttp_session
except ImportError:
aiohttp_session = None # type: ignore[assignment]
from typing import Any
@@ -50,7 +55,7 @@ async def check_authorized(*args: Any, **kwargs: Any) -> Any:
Args:
*args(Any): argument list as provided by check_authorized function
**kwargs(Any): named argument list as provided by authorized_userid function
**kwargs(Any): named argument list as provided by check_authorized function
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
@@ -66,7 +71,7 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
Args:
*args(Any): argument list as provided by forget function
**kwargs(Any): named argument list as provided by authorized_userid function
**kwargs(Any): named argument list as provided by forget function
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise
@@ -76,13 +81,29 @@ async def forget(*args: Any, **kwargs: Any) -> Any:
return None
async def get_session(*args: Any, **kwargs: Any) -> Any:
"""
handle aiohttp session methods
Args:
*args(Any): argument list as provided by get_session function
**kwargs(Any): named argument list as provided by get_session function
Returns:
Any: empty dictionary in case if no aiohttp_session module found and function call otherwise
"""
if aiohttp_session is not None:
return await aiohttp_session.get_session(*args, **kwargs)
return {}
async def remember(*args: Any, **kwargs: Any) -> Any:
"""
handle disabled auth
Args:
*args(Any): argument list as provided by remember function
**kwargs(Any): named argument list as provided by authorized_userid function
**kwargs(Any): named argument list as provided by remember function
Returns:
Any: ``None`` in case if no aiohttp_security module found and function call otherwise

View File

@@ -19,6 +19,8 @@
#
import aioauth_client
from typing import Any
from ahriman.core.auth.mapping import Mapping
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
@@ -53,7 +55,7 @@ class OAuth(Mapping):
self.client_secret = configuration.get("auth", "client_secret")
# in order to use OAuth feature the service must be publicity available
# thus we expect that address is set
self.redirect_uri = f"""{configuration.get("web", "address")}/api/v1/login"""
self.redirect_uri = f"{configuration.get("web", "address")}/api/v1/login"
self.provider = self.get_provider(configuration.get("auth", "oauth_provider"))
# it is list, but we will have to convert to string it anyway
self.scopes = configuration.get("auth", "oauth_scopes")
@@ -67,8 +69,8 @@ class OAuth(Mapping):
Returns:
str: login control as html code to insert
"""
return f"""<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-{
self.icon}"></i> login</a>"""
return f"<a class=\"nav-link\" href=\"/api/v1/login\" title=\"login via OAuth2\"><i class=\"bi bi-{
self.icon}\"></i> login</a>"
@staticmethod
def get_provider(name: str) -> type[aioauth_client.OAuth2Client]:
@@ -84,7 +86,7 @@ class OAuth(Mapping):
Raises:
OptionError: in case if invalid OAuth provider name supplied
"""
provider: type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
provider: type = getattr(aioauth_client, name, type(None))
try:
is_oauth2_client = issubclass(provider, aioauth_client.OAuth2Client)
except TypeError: # what if it is random string?
@@ -102,27 +104,35 @@ class OAuth(Mapping):
"""
return self.provider(client_id=self.client_id, client_secret=self.client_secret)
def get_oauth_url(self) -> str:
def get_oauth_url(self, state: str) -> str:
"""
get authorization URI for the specified settings
Args:
state(str): CSRF token to pass to OAuth2 provider
Returns:
str: authorization URI as a string
"""
client = self.get_client()
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri)
uri: str = client.get_authorize_url(scope=self.scopes, redirect_uri=self.redirect_uri, state=state)
return uri
async def get_oauth_username(self, code: str) -> str | None:
async def get_oauth_username(self, code: str, state: str | None, session: dict[str, Any]) -> str | None:
"""
extract OAuth username from remote
Args:
code(str): authorization code provided by external service
state(str | None): CSRF token returned by external service
session(dict[str, Any]): current session instance
Returns:
str | None: username as is in OAuth provider
"""
if state is None or state != session.get("state"):
return None
try:
client = self.get_client()
access_token, _ = await client.get_access_token(code, redirect_uri=self.redirect_uri)

View File

@@ -0,0 +1,114 @@
#
# Copyright (c) 2021-2026 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 ahriman.core.build_tools.task import Task
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.utils import full_version, utcnow
from ahriman.models.package import Package
from ahriman.models.pkgbuild import Pkgbuild
class PackageVersion(LazyLogging):
"""
package version extractor and helper
Attributes:
package(Package): package definitions
"""
def __init__(self, package: Package) -> None:
"""
Args:
package(Package): package definitions
"""
self.package = package
def actual_version(self, configuration: Configuration) -> str:
"""
additional method to handle VCS package versions
Args:
configuration(Configuration): configuration instance
Returns:
str: package version if package is not VCS and current version according to VCS otherwise
"""
if not self.package.is_vcs:
return self.package.version
_, repository_id = configuration.check_loaded()
paths = configuration.repository_paths
task = Task(self.package, configuration, repository_id.architecture, paths)
try:
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
task.init(paths.cache_for(self.package.base), [], None)
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.package.base) / "PKGBUILD")
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
except Exception:
self.logger.exception("cannot determine version of VCS package")
finally:
# clear log files generated by devtools
for log_file in paths.cache_for(self.package.base).glob("*.log"):
log_file.unlink()
return self.package.version
def is_newer_than(self, timestamp: float | int) -> bool:
"""
check if package was built after the specified timestamp
Args:
timestamp(float | int): timestamp to check build date against
Returns:
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
In case if build date is not set by any of packages, it returns False
"""
return any(
package.build_date > timestamp
for package in self.package.packages.values()
if package.build_date is not None
)
def is_outdated(self, remote: Package, configuration: Configuration, *,
calculate_version: bool = True) -> bool:
"""
check if package is out-of-dated
Args:
remote(Package): package properties from remote source
configuration(Configuration): configuration instance
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
(Default value = True)
Returns:
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
"""
vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
if calculate_version and not self.is_newer_than(min_vcs_build_date):
remote_version = PackageVersion(remote).actual_version(configuration)
else:
remote_version = remote.version
return self.package.vercmp(remote_version) < 0

View File

@@ -19,7 +19,7 @@
#
import shutil
from collections.abc import Generator
from collections.abc import Iterator
from pathlib import Path
from typing import ClassVar
@@ -27,6 +27,7 @@ from ahriman.core.exceptions import CalledProcessError
from ahriman.core.log import LazyLogging
from ahriman.core.utils import check_output, utcnow, walk
from ahriman.models.package import Package
from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_paths import RepositoryPaths
@@ -81,7 +82,7 @@ class Sources(LazyLogging):
Returns:
list[PkgbuildPatch]: generated patch for PKGBUILD architectures if required
"""
architectures = Package.supported_architectures(sources_dir)
architectures = Pkgbuild.supported_architectures(sources_dir)
if "any" in architectures: # makepkg does not like when there is any other arch except for any
return []
architectures.add(architecture)
@@ -161,7 +162,7 @@ class Sources(LazyLogging):
cwd=sources_dir, logger=instance.logger)
# extract local files...
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)]
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Pkgbuild.local_files(sources_dir)]
instance.add(sources_dir, *files)
# ...and commit them
instance.commit(sources_dir)
@@ -347,7 +348,7 @@ class Sources(LazyLogging):
"""
gitconfig = gitconfig or {}
def configuration_flags() -> Generator[str, None, None]:
def configuration_flags() -> Iterator[str]:
for option, value in (self.GITCONFIG | gitconfig).items():
yield "-c"
yield f"{option}=\"{value}\""

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections.abc import Generator
from collections.abc import Iterator
from pathlib import Path
from ahriman.core.build_tools.sources import Sources
@@ -77,7 +77,7 @@ class Task(LazyLogging):
Returns:
list[Path]: list of file paths which looks like freshly generated archives
"""
def files() -> Generator[Path, None, None]:
def files() -> Iterator[Path]:
for filepath in sources_dir.iterdir():
if filepath in source_files:
continue # skip files which were already there

View File

@@ -97,6 +97,26 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
},
},
},
"aur": {
"type": "dict",
"schema": {
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
},
"auth": {
"type": "dict",
"schema": {
@@ -296,10 +316,20 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False,
"is_url": [],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"password": {
"type": "string",
"empty": False,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",

View File

@@ -21,7 +21,7 @@ import configparser
import os
import sys
from collections.abc import Generator, Mapping, MutableMapping
from collections.abc import Iterator, Mapping, MutableMapping
from string import Template
from typing import Any, ClassVar
@@ -37,7 +37,7 @@ class ShellInterpolator(configparser.Interpolation):
@staticmethod
def _extract_variables(parser: MutableMapping[str, Mapping[str, str]], value: str,
defaults: Mapping[str, str]) -> Generator[tuple[str, str], None, None]:
defaults: Mapping[str, str]) -> Iterator[tuple[str, str]]:
"""
extract keys and values (if available) from the configuration. In case if a key is not available, it will be
silently skipped from the result
@@ -50,7 +50,7 @@ class ShellInterpolator(configparser.Interpolation):
Yields:
tuple[str, str]: variable name used for substitution and its value
"""
def identifiers() -> Generator[tuple[str | None, str], None, None]:
def identifiers() -> Iterator[tuple[str | None, str]]:
# extract all found identifiers and parse them
for identifier in ShellTemplate(value).get_identifiers():
match identifier.rsplit(":", maxsplit=1):

View File

@@ -20,7 +20,7 @@
import fnmatch
import re
from collections.abc import Generator, Mapping
from collections.abc import Iterator, Mapping
from string import Template
@@ -132,7 +132,7 @@ class ShellTemplate(Template):
(self._REPLACE, self._replace, "/"),
)
def generator(variables: dict[str, str]) -> Generator[tuple[str, str], None, None]:
def generator(variables: dict[str, str]) -> Iterator[tuple[str, str]]:
for identifier in self.get_identifiers():
for regex, function, greediness in substitutions:
if m := regex.match(identifier):

View File

@@ -76,6 +76,19 @@ class Validator(RootValidator):
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore[attr-defined]
return converted
def _normalize_coerce_float(self, value: str) -> float:
"""
extract float from string value
Args:
value(str): converting value
Returns:
float: value converted to float according to configuration rules
"""
del self
return float(value)
def _normalize_coerce_integer(self, value: str) -> int:
"""
extract integer from string value

View File

@@ -17,14 +17,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from dataclasses import replace
from sqlite3 import Connection
from ahriman.application.handlers.handler import Handler
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Explorer
from ahriman.core.sign.gpg import GPG
from ahriman.core.utils import atomic_move, package_like, symlink_relative
from ahriman.models.package import Package
from ahriman.models.pacman_synchronization import PacmanSynchronization
from ahriman.models.repository_paths import RepositoryPaths
@@ -43,16 +43,13 @@ def migrate_data(connection: Connection, configuration: Configuration) -> None:
"""
del connection
config_path, _ = configuration.check_loaded()
args = argparse.Namespace(configuration=config_path, architecture=None, repository=None, repository_id=None)
for repository_id in Handler.repositories_extract(args):
for repository_id in Explorer.repositories_extract(configuration):
paths = replace(configuration.repository_paths, repository_id=repository_id)
pacman = Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
# create archive directory if required
if not paths.archive.is_dir():
with paths.preserve_owner(paths.archive):
with paths.preserve_owner():
paths.archive.mkdir(mode=0o755, parents=True)
move_packages(paths, pacman)
@@ -66,19 +63,19 @@ def move_packages(repository_paths: RepositoryPaths, pacman: Pacman) -> None:
repository_paths(RepositoryPaths): repository paths instance
pacman(Pacman): alpm wrapper instance
"""
for source in repository_paths.repository.iterdir():
if not source.is_file(follow_symlinks=False):
for archive in filter(package_like, repository_paths.repository.iterdir()):
if not archive.is_file(follow_symlinks=False):
continue # skip symbolic links if any
filename = source.name
if filename.startswith(".") or ".pkg." not in filename:
# we don't use package_like method here, because it also filters out signatures
continue
package = Package.from_archive(source, pacman)
package = Package.from_archive(archive, pacman)
artifacts = [archive]
# check if there are signatures for this package and append it here too
if (signature := GPG.signature(archive)).exists():
artifacts.append(signature)
# move package to the archive directory
target = repository_paths.archive_for(package.base) / filename
source.rename(target)
# create symlink to the archive
source.symlink_to(target.relative_to(source.parent, walk_up=True))
for source in artifacts:
target = repository_paths.ensure_exists(repository_paths.archive_for(package.base)) / source.name
# move package to the archive directory
atomic_move(source, target)
# create symlink to the archive
symlink_relative(source, target)

View File

@@ -141,14 +141,15 @@ class LogsOperations(Operations):
connection.execute(
"""
delete from logs
where (package_base, version, repository, process_id) not in (
select package_base, version, repository, process_id from logs
where (package_base, version, repository, created) in (
select package_base, version, repository, max(created) from logs
where repository = :repository
group by package_base, version, repository
where repository = :repository
and (package_base, version, repository, process_id) not in (
select package_base, version, repository, process_id from logs
where (package_base, version, repository, created) in (
select package_base, version, repository, max(created) from logs
where repository = :repository
group by package_base, version, repository
)
)
)
""",
{
"repository": repository_id.id,

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections.abc import Generator, Iterable
from collections.abc import Iterable, Iterator
from sqlite3 import Connection
from ahriman.core.database.operations.operations import Operations
@@ -263,7 +263,7 @@ class PackageOperations(Operations):
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> Generator[tuple[Package, BuildStatus], None, None]:
def run(connection: Connection) -> Iterator[tuple[Package, BuildStatus]]:
packages = self._packages_get_select_package_bases(connection, repository_id)
statuses = self._packages_get_select_statuses(connection, repository_id)
per_package_base = self._packages_get_select_packages(connection, packages, repository_id)

View File

@@ -20,7 +20,6 @@
import subprocess
from collections.abc import Callable
from pathlib import Path
from typing import Any, Self
from ahriman.models.repository_id import RepositoryId
@@ -229,20 +228,6 @@ class PkgbuildParserError(ValueError):
ValueError.__init__(self, message)
class PathError(ValueError):
"""
exception which will be raised on path which is not belong to root directory
"""
def __init__(self, path: Path, root: Path) -> None:
"""
Args:
path(Path): path which raised an exception
root(Path): repository root (i.e. ahriman home)
"""
ValueError.__init__(self, f"Path `{path}` does not belong to repository root `{root}`")
class PasswordError(ValueError):
"""
exception which will be raised in case of password related errors

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from collections.abc import Generator
from collections.abc import Iterator
from typing import Any
from ahriman.core.formatters.string_printer import StringPrinter
@@ -44,8 +44,7 @@ class ValidationPrinter(StringPrinter):
self.errors = errors
@staticmethod
def get_error_messages(node: str, errors: list[str | dict[str, Any]],
current_level: int = 1) -> Generator[Property, None, None]:
def get_error_messages(node: str, errors: list[str | dict[str, Any]], current_level: int = 1) -> Iterator[Property]:
"""
extract default error message from cerberus class

View File

@@ -48,6 +48,10 @@ class RemotePullTrigger(Trigger):
"gitremote": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["gitremote"],
},
"pull_url": {
"type": "string",
"required": True,
@@ -60,7 +64,6 @@ class RemotePullTrigger(Trigger):
},
},
}
CONFIGURATION_SCHEMA_FALLBACK = "gitremote"
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
"""
@@ -89,7 +92,6 @@ class RemotePullTrigger(Trigger):
trigger action which will be called at the start of the application
"""
for target in self.targets:
section, _ = self.configuration.gettype(
target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
section, _ = self.configuration.gettype(target, self.repository_id, fallback="gitremote")
runner = RemotePull(self.repository_id, self.configuration, section)
runner.run()

View File

@@ -19,7 +19,7 @@
#
import shutil
from collections.abc import Generator
from collections.abc import Iterator
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -96,7 +96,7 @@ class RemotePush(LazyLogging):
# ...and finally return path to the copied directory
return package.base
def packages_update(self, result: Result, target_dir: Path) -> Generator[str, None, None]:
def packages_update(self, result: Result, target_dir: Path) -> Iterator[str]:
"""
update all packages from the build result

View File

@@ -52,6 +52,10 @@ class RemotePushTrigger(Trigger):
"gitremote": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["gitremote"],
},
"commit_email": {
"type": "string",
"empty": False,
@@ -72,7 +76,6 @@ class RemotePushTrigger(Trigger):
},
},
}
CONFIGURATION_SCHEMA_FALLBACK = "gitremote"
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
"""
@@ -111,7 +114,6 @@ class RemotePushTrigger(Trigger):
reporter = ctx.get(Client)
for target in self.targets:
section, _ = self.configuration.gettype(
target, self.repository_id, fallback=self.CONFIGURATION_SCHEMA_FALLBACK)
section, _ = self.configuration.gettype(target, self.repository_id, fallback="gitremote")
runner = RemotePush(reporter, self.configuration, section)
runner.run(result)

View File

@@ -94,6 +94,7 @@ class ArchiveRotationTrigger(Trigger):
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
to_remove = sorted(packages.values(), key=cmp_to_key(comparator))
# 0 will implicitly be translated into [:0], meaning we keep all packages
for single in to_remove[:-self.keep_built_packages]:
self.logger.info("removing version %s of package %s", single.version, single.base)
for archive in single.packages.values():

View File

@@ -20,10 +20,9 @@
import contextlib
import requests
from functools import cached_property
from requests.adapters import BaseAdapter
from urllib.parse import urlparse
from ahriman import __version__
from ahriman.core.http.sync_http_client import SyncHttpClient
@@ -37,32 +36,36 @@ class SyncAhrimanClient(SyncHttpClient):
address: str
@cached_property
def session(self) -> requests.Session:
def _login_url(self) -> str:
"""
get or create session
get url for the login api
Returns:
request.Session: created session object
str: full url for web service to log in
"""
if urlparse(self.address).scheme == "http+unix":
import requests_unixsocket
session: requests.Session = requests_unixsocket.Session() # type: ignore[no-untyped-call]
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session
return f"{self.address}/api/v1/login"
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
self._login(session)
return session
def _login(self, session: requests.Session) -> None:
def adapters(self) -> dict[str, BaseAdapter]:
"""
process login to the service
get registered adapters
Returns:
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
"""
adapters = SyncHttpClient.adapters(self)
if (scheme := urlparse(self.address).scheme) == "http+unix":
from requests_unixsocket.adapters import UnixAdapter
adapters[f"{scheme}://"] = UnixAdapter() # type: ignore[no-untyped-call]
return adapters
def on_session_creation(self, session: requests.Session) -> None:
"""
method which will be called on session creation
Args:
session(requests.Session): request session to login
session(requests.Session): created requests session
"""
if self.auth is None:
return # no auth configured
@@ -74,12 +77,3 @@ class SyncAhrimanClient(SyncHttpClient):
}
with contextlib.suppress(Exception):
self.make_request("POST", self._login_url(), json=payload, session=session)
def _login_url(self) -> str:
"""
get url for the login api
Returns:
str: full url for web service to log in
"""
return f"{self.address}/api/v1/login"

View File

@@ -21,7 +21,9 @@ import requests
import sys
from functools import cached_property
from typing import Any, IO, Literal
from requests.adapters import BaseAdapter, HTTPAdapter
from typing import Any, ClassVar, IO, Literal
from urllib3.util.retry import Retry
from ahriman import __version__
from ahriman.core.configuration import Configuration
@@ -37,11 +39,19 @@ class SyncHttpClient(LazyLogging):
wrapper around requests library to reduce boilerplate
Attributes:
DEFAULT_MAX_RETRIES(int): (class attribute) default maximum amount of retries
DEFAULT_RETRY_BACKOFF(float): (class attribute) default retry exponential backoff
DEFAULT_TIMEOUT(int | None): (class attribute) default HTTP request timeout in seconds
auth(tuple[str, str] | None): HTTP basic auth object if set
retry(Retry): retry policy of the HTTP client
suppress_errors(bool): suppress logging of request errors
timeout(int | None): HTTP request timeout in seconds
"""
DEFAULT_MAX_RETRIES: ClassVar[int] = 0
DEFAULT_RETRY_BACKOFF: ClassVar[float] = 0.0
DEFAULT_TIMEOUT: ClassVar[int | None] = 30
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
suppress_errors: bool = False) -> None:
"""
@@ -50,18 +60,21 @@ class SyncHttpClient(LazyLogging):
section(str | None, optional): settings section name (Default value = None)
suppress_errors(bool, optional): suppress logging of request errors (Default value = False)
"""
if configuration is None:
configuration = Configuration() # dummy configuration
if section is None:
section = configuration.default_section
configuration = configuration or Configuration() # dummy configuration
section = section or configuration.default_section
username = configuration.get(section, "username", fallback=None)
password = configuration.get(section, "password", fallback=None)
self.auth = (username, password) if username and password else None
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
self.suppress_errors = suppress_errors
self.timeout = configuration.getint(section, "timeout", fallback=self.DEFAULT_TIMEOUT)
self.retry = SyncHttpClient.retry_policy(
max_retries=configuration.getint(section, "max_retries", fallback=self.DEFAULT_MAX_RETRIES),
retry_backoff=configuration.getfloat(section, "retry_backoff", fallback=self.DEFAULT_RETRY_BACKOFF),
)
@cached_property
def session(self) -> requests.Session:
"""
@@ -71,11 +84,17 @@ class SyncHttpClient(LazyLogging):
request.Session: created session object
"""
session = requests.Session()
for protocol, adapter in self.adapters().items():
session.mount(protocol, adapter)
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
session.headers["User-Agent"] = f"ahriman/{__version__} " \
f"{requests.utils.default_user_agent()} " \
f"python/{python_version}"
self.on_session_creation(session)
return session
@staticmethod
@@ -92,6 +111,39 @@ class SyncHttpClient(LazyLogging):
result: str = exception.response.text if exception.response is not None else ""
return result
@staticmethod
def retry_policy(max_retries: int = 0, retry_backoff: float = 0.0) -> Retry:
"""
build retry policy for class
Args:
max_retries(int, optional): maximum amount of retries allowed (Default value = 0)
retry_backoff(float, optional): retry exponential backoff (Default value = 0.0)
Returns:
Retry: built retry policy
"""
return Retry(
total=max_retries,
connect=max_retries,
read=max_retries,
status=max_retries,
status_forcelist=[429, 500, 502, 503, 504],
backoff_factor=retry_backoff,
)
def adapters(self) -> dict[str, BaseAdapter]:
"""
get registered adapters
Returns:
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
"""
return {
"http://": HTTPAdapter(max_retries=self.retry),
"https://": HTTPAdapter(max_retries=self.retry),
}
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
headers: dict[str, str] | None = None,
params: list[tuple[str, str]] | None = None,
@@ -139,3 +191,11 @@ class SyncHttpClient(LazyLogging):
if not suppress_errors:
self.logger.exception("could not perform http request")
raise
def on_session_creation(self, session: requests.Session) -> None:
"""
method which will be called on session creation
Args:
session(requests.Session): created requests session
"""

View File

@@ -20,7 +20,7 @@
import contextlib
import logging
from collections.abc import Generator
from collections.abc import Iterator
from functools import cached_property
from typing import Any
@@ -80,7 +80,7 @@ class LazyLogging:
logging.setLogRecordFactory(package_record_factory)
@contextlib.contextmanager
def in_package_context(self, package_base: str, version: str | None) -> Generator[None, None, None]:
def in_package_context(self, package_base: str, version: str | None) -> Iterator[None]:
"""
execute function while setting package context

View File

@@ -19,7 +19,7 @@
#
import inspect
from collections.abc import Generator
from collections.abc import Iterator
from importlib import import_module
from pathlib import Path
from pkgutil import ModuleInfo, walk_packages
@@ -33,7 +33,7 @@ __all__ = ["implementations"]
T = TypeVar("T")
def _modules(module_root: Path, prefix: str) -> Generator[ModuleInfo, None, None]:
def _modules(module_root: Path, prefix: str) -> Iterator[ModuleInfo]:
"""
extract available modules from package
@@ -52,7 +52,7 @@ def _modules(module_root: Path, prefix: str) -> Generator[ModuleInfo, None, None
yield module_info
def implementations(root_module: ModuleType, base_class: type[T]) -> Generator[type[T], None, None]:
def implementations(root_module: ModuleType, base_class: type[T]) -> Iterator[type[T]]:
"""
extract implementations of the ``base_class`` from the module

View File

@@ -74,6 +74,18 @@ class Email(Report, JinjaTemplate):
self.ssl = SmtpSSLSettings.from_option(configuration.get(section, "ssl", fallback="disabled"))
self.user = configuration.get(section, "user", fallback=None)
@property
def _smtp_session(self) -> type[smtplib.SMTP]:
"""
build SMTP session based on configuration settings
Returns:
type[smtplib.SMTP]: SMTP or SMTP_SSL session depending on whether SSL is enabled or not
"""
if self.ssl == SmtpSSLSettings.SSL:
return smtplib.SMTP_SSL
return smtplib.SMTP
def _send(self, text: str, attachment: dict[str, str]) -> None:
"""
send email callback
@@ -93,16 +105,13 @@ class Email(Report, JinjaTemplate):
attach.add_header("Content-Disposition", "attachment", filename=filename)
message.attach(attach)
if self.ssl != SmtpSSLSettings.SSL:
session = smtplib.SMTP(self.host, self.port)
with self._smtp_session(self.host, self.port) as session:
if self.ssl == SmtpSSLSettings.STARTTLS:
session.starttls()
else:
session = smtplib.SMTP_SSL(self.host, self.port)
if self.user is not None and self.password is not None:
session.login(self.user, self.password)
session.sendmail(self.sender, self.receivers, message.as_string())
session.quit()
if self.user is not None and self.password is not None:
session.login(self.user, self.password)
session.sendmail(self.sender, self.receivers, message.as_string())
def generate(self, packages: list[Package], result: Result) -> None:
"""

View File

@@ -302,6 +302,16 @@ class ReportTrigger(Trigger):
"empty": False,
"is_url": [],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"rss_url": {
"type": "string",
"empty": False,

View File

@@ -17,4 +17,5 @@
# 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 ahriman.core.repository.explorer import Explorer
from ahriman.core.repository.repository import Repository

View File

@@ -19,7 +19,7 @@
#
import contextlib
from typing import Generator
from collections.abc import Iterator
from ahriman.core.status import Client
from ahriman.models.event import Event, EventType
@@ -55,7 +55,7 @@ class EventLogger:
@contextlib.contextmanager
def in_event(self, package_base: str, event: EventType, message: str | None = None,
failure: EventType | None = None) -> Generator[None, None, None]:
failure: EventType | None = None) -> Iterator[None]:
"""
perform action in package context and log event with time elapsed

View File

@@ -19,7 +19,7 @@
#
import shutil
from collections.abc import Generator, Iterable
from collections.abc import Iterable
from pathlib import Path
from tempfile import TemporaryDirectory
@@ -27,7 +27,7 @@ from ahriman.core.build_tools.package_archive import PackageArchive
from ahriman.core.build_tools.task import Task
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.package_info import PackageInfo
from ahriman.core.utils import atomic_move, filelock, package_like, safe_filename
from ahriman.core.utils import atomic_move, filelock, list_flatmap, package_like, safe_filename, symlink_relative
from ahriman.models.changes import Changes
from ahriman.models.event import EventType
from ahriman.models.package import Package
@@ -41,35 +41,34 @@ class Executor(PackageInfo, Cleaner):
trait for common repository update processes
"""
def _archive_lookup(self, package: Package) -> Generator[Path, None, None]:
def _archive_lookup(self, package: Package) -> list[Path]:
"""
check if there is a rebuilt package already
Args:
package(Package): package to check
Yields:
Path: list of built packages and signatures if available, empty list otherwise
Returns:
list[Path]: list of built packages and signatures if available, empty list otherwise
"""
archive = self.paths.archive_for(package.base)
if not archive.is_dir():
return []
# find all packages which have same version
same_version = [
built
for path in filter(package_like, archive.iterdir())
if (built := Package.from_archive(path, self.pacman)).version == package.version
]
# no packages of the same version found
if not same_version:
return
for path in filter(package_like, archive.iterdir()):
# check if package version is the same
built = Package.from_archive(path, self.pacman)
if built.version != package.version:
continue
packages = [single for built in same_version for single in built.packages.values()]
# all packages must be either any or same architecture
if not all(single.architecture in ("any", self.architecture) for single in packages):
return
packages = built.packages.values()
# all packages must be either any or same architecture
if not all(single.architecture in ("any", self.architecture) for single in packages):
continue
for single in packages:
yield from archive.glob(f"{single.filename}*")
return list_flatmap(packages, lambda single: archive.glob(f"{single.filename}*"))
return []
def _archive_rename(self, description: PackageDescription, package_base: str) -> None:
"""
@@ -111,10 +110,10 @@ class Executor(PackageInfo, Cleaner):
if prebuilt := list(self._archive_lookup(loaded_package)):
self.logger.info("using prebuilt packages for %s-%s", loaded_package.base, loaded_package.version)
built = []
for artefact in prebuilt:
with filelock(artefact):
shutil.copy(artefact, path)
built.append(path / artefact.name)
for artifact in prebuilt:
with filelock(artifact):
shutil.copy(artifact, path)
built.append(path / artifact.name)
else:
built = task.build(path, PACKAGER=packager)
@@ -143,7 +142,7 @@ class Executor(PackageInfo, Cleaner):
remove package base from repository
Args:
package_base(str): package base name:
package_base(str): package base name
"""
try:
with self.in_event(package_base, EventType.PackageRemoved):
@@ -169,10 +168,10 @@ class Executor(PackageInfo, Cleaner):
files = self.sign.process_sign_package(full_path, packager_key)
for src in files:
dst = self.paths.archive_for(package_base) / src.name
dst = self.paths.ensure_exists(self.paths.archive_for(package_base)) / src.name
atomic_move(src, dst) # move package to archive directory
if not (symlink := self.paths.repository / dst.name).exists():
symlink.symlink_to(dst.relative_to(symlink.parent, walk_up=True)) # create link to archive
symlink_relative(symlink, dst) # create link to archive
self.repo.add(self.paths.repository / filename)

View File

@@ -0,0 +1,70 @@
#
# Copyright (c) 2021-2026 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# 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 collections.abc import Iterable
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
from ahriman.models.repository_paths import RepositoryPaths
class Explorer:
"""
helper to read filesystem and find created repositories
"""
@staticmethod
def repositories_extract(configuration: Configuration, repository: str | None = None,
architecture: str | None = None) -> list[RepositoryId]:
"""
get known architectures
Args:
configuration(Configuration): configuration instance
repository(str | None, optional): predefined repository name if available (Default value = None)
architecture(str | None, optional): predefined repository architecture if available (Default value = None)
Returns:
list[RepositoryId]: list of repository names and architectures for which tree is created
"""
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# extract repository names first
if repository is not None:
repositories: Iterable[str] = [repository]
elif from_filesystem := RepositoryPaths.known_repositories(root):
repositories = from_filesystem
else: # try to read configuration now
repositories = [configuration.get("repository", "name")]
# extract architecture names
if architecture is not None:
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
)
else: # try to read from file system
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
for architecture in RepositoryPaths.known_architectures(root, repository)
)
return sorted(parsed)

View File

@@ -17,10 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import copy
from collections.abc import Iterable
from pathlib import Path
from tempfile import TemporaryDirectory
from ahriman.core.build_tools.package_version import PackageVersion
from ahriman.core.build_tools.sources import Sources
from ahriman.core.repository.repository_properties import RepositoryProperties
from ahriman.core.utils import package_like
@@ -33,6 +36,40 @@ class PackageInfo(RepositoryProperties):
handler for the package information
"""
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
"""
generate full dependencies list including transitive dependencies
Args:
package(Package): package to check dependencies for
packages(Iterable[Package]): repository package list
Returns:
list[str]: all dependencies of the package
"""
dependencies = {}
# load own package dependencies
for package_base in packages:
for name, repo_package in package_base.packages.items():
dependencies[name] = repo_package.depends
for provides in repo_package.provides:
dependencies[provides] = repo_package.depends
# load repository dependencies
for database in self.pacman.handle.get_syncdbs():
for pacman_package in database.pkgcache:
dependencies[pacman_package.name] = pacman_package.depends
for provides in pacman_package.provides:
dependencies[provides] = pacman_package.depends
result = set(package.depends)
current_depends: set[str] = set()
while result != current_depends:
current_depends = copy.deepcopy(result)
for package_name in current_depends:
result.update(dependencies.get(package_name, []))
return sorted(result)
def load_archives(self, packages: Iterable[Path]) -> list[Package]:
"""
load packages from list of archives
@@ -58,7 +95,7 @@ class PackageInfo(RepositoryProperties):
# force version to max of them
self.logger.warning("version of %s differs, found %s and %s",
current.base, current.version, local.version)
if current.is_outdated(local, self.configuration, calculate_version=False):
if PackageVersion(current).is_outdated(local, self.configuration, calculate_version=False):
current.version = local.version
current.packages.update(local.packages)
except Exception:
@@ -130,5 +167,5 @@ class PackageInfo(RepositoryProperties):
return [
package
for package in packages
if depends_on.intersection(package.full_depends(self.pacman, packages))
if depends_on.intersection(self.full_depends(package, packages))
]

View File

@@ -21,6 +21,7 @@ from typing import Self
from ahriman.core import _Context, context
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.repository.executor import Executor
@@ -73,9 +74,24 @@ class Repository(Executor, UpdateHandler):
"""
instance = cls(repository_id, configuration, database,
report=report, refresh_pacman_database=refresh_pacman_database)
instance._set_globals(configuration)
instance._set_context()
return instance
@staticmethod
def _set_globals(configuration: Configuration) -> None:
"""
set global settings based on configuration via class attributes
Args:
configuration(Configuration): configuration instance
"""
AUR.DEFAULT_MAX_RETRIES = configuration.getint("aur", "max_retries", fallback=0)
AUR.DEFAULT_RETRY_BACKOFF = configuration.getfloat("aur", "retry_backoff", fallback=0.0)
AUR.DEFAULT_TIMEOUT = configuration.getint("aur", "timeout", fallback=30)
def _set_context(self) -> None:
"""
create context variables and set their values

View File

@@ -19,10 +19,12 @@
#
from collections.abc import Iterable
from ahriman.core.build_tools.package_version import PackageVersion
from ahriman.core.build_tools.sources import Sources
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository.cleaner import Cleaner
from ahriman.core.repository.package_info import PackageInfo
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.event import EventType
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
@@ -67,10 +69,12 @@ class UpdateHandler(PackageInfo, Cleaner):
try:
remote = load_remote(local)
if local.is_outdated(remote, self.configuration, calculate_version=vcs):
if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs):
self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Remote version is newer than local")
result.append(remote)
else:
self.reporter.package_status_update(local.base, BuildStatusEnum.Success)
except Exception:
self.reporter.set_failed(local.base)
self.logger.exception("could not load remote package %s", local.base)
@@ -79,7 +83,8 @@ class UpdateHandler(PackageInfo, Cleaner):
def updates_dependencies(self, filter_packages: Iterable[str]) -> list[Package]:
"""
check packages which ae required to be rebuilt based on dynamic dependencies (e.g. linking, modules paths, etc.)
check packages which are required to be rebuilt based on dynamic dependencies
(e.g. linking, modules paths, etc.)
Args:
filter_packages(Iterable[str]): do not check every package just specified in the list
@@ -153,7 +158,7 @@ class UpdateHandler(PackageInfo, Cleaner):
if local.remote.is_remote:
continue # avoid checking AUR packages
if local.is_outdated(remote, self.configuration, calculate_version=vcs):
if PackageVersion(local).is_outdated(remote, self.configuration, calculate_version=vcs):
self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Locally pulled sources are outdated")
result.append(remote)
@@ -180,8 +185,9 @@ class UpdateHandler(PackageInfo, Cleaner):
else:
self.reporter.set_pending(local.base)
self.event(local.base, EventType.PackageOutdated, "Manual update is requested")
self.clear_queue()
except Exception:
self.logger.exception("could not load packages from database")
self.clear_queue()
return result

View File

@@ -20,7 +20,7 @@
import hashlib
import itertools
from collections.abc import Callable, Generator
from collections.abc import Callable, Iterator
from pathlib import Path
from typing import ClassVar
@@ -187,7 +187,7 @@ class PkgbuildGenerator:
Returns:
list[PkgbuildPatch]: list of patches to be applied to the PKGBUILD
"""
def sources_generator() -> Generator[tuple[str, str], None, None]:
def sources_generator() -> Iterator[tuple[str, str]]:
for source, generator in sorted(self.sources().items()):
source_path = source_dir / source
generator(source_path)

View File

@@ -34,8 +34,6 @@ class Trigger(LazyLogging):
Attributes:
CONFIGURATION_SCHEMA(ConfigurationSchema): (class attribute) configuration schema template
CONFIGURATION_SCHEMA_FALLBACK(str | None): (class attribute) optional fallback option for defining
configuration schema type used
REQUIRES_REPOSITORY(bool): (class attribute) either trigger requires loaded repository or not
configuration(Configuration): configuration instance
repository_id(RepositoryId): repository unique identifier
@@ -59,7 +57,6 @@ class Trigger(LazyLogging):
"""
CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {}
CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None
REQUIRES_REPOSITORY: ClassVar[bool] = True
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:

View File

@@ -21,7 +21,7 @@ import atexit
import contextlib
import os
from collections.abc import Generator
from collections.abc import Iterator
from importlib import import_module, machinery
from pathlib import Path
from types import ModuleType
@@ -112,7 +112,7 @@ class TriggerLoader(LazyLogging):
return configuration.getlist("build", "triggers", fallback=[])
@contextlib.contextmanager
def __execute_trigger(self, trigger: Trigger) -> Generator[None, None, None]:
def __execute_trigger(self, trigger: Trigger) -> Iterator[None]:
"""
decorator for calling triggers

View File

@@ -54,6 +54,11 @@ class UploadTrigger(Trigger):
"type": "string",
"allowed": ["github"],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"owner": {
"type": "string",
"required": True,
@@ -68,6 +73,11 @@ class UploadTrigger(Trigger):
"required": True,
"empty": False,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",
@@ -90,6 +100,16 @@ class UploadTrigger(Trigger):
"type": "string",
"allowed": ["ahriman", "remote-service"],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",

View File

@@ -20,7 +20,6 @@
# pylint: disable=too-many-lines
import contextlib
import datetime
import fcntl
import io
import itertools
import logging
@@ -30,15 +29,16 @@ import selectors
import shutil
import subprocess
from collections.abc import Callable, Generator, Iterable, Mapping
from collections.abc import Callable, Iterable, Iterator, Mapping
from dataclasses import asdict
from enum import Enum
from filelock import FileLock
from pathlib import Path
from pwd import getpwuid
from typing import Any, IO, TypeVar
from ahriman.core.exceptions import CalledProcessError, OptionError, UnsafeRunError
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.core.types import Comparable
__all__ = [
@@ -51,7 +51,9 @@ __all__ = [
"filelock",
"filter_json",
"full_version",
"list_flatmap",
"minmax",
"owner",
"package_like",
"parse_version",
"partition",
@@ -61,12 +63,14 @@ __all__ = [
"safe_filename",
"srcinfo_property",
"srcinfo_property_list",
"symlink_relative",
"trim_package",
"utcnow",
"walk",
]
R = TypeVar("R", bound=Comparable)
T = TypeVar("T")
@@ -137,7 +141,7 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
return channel if channel is not None else io.StringIO()
# wrapper around selectors polling
def poll(sel: selectors.BaseSelector) -> Generator[tuple[str, str], None, None]:
def poll(sel: selectors.BaseSelector) -> Iterator[tuple[str, str]]:
for key, _ in sel.select(): # we don't need to check mask here because we have only subscribed on reading
line = key.fileobj.readline() # type: ignore[union-attr]
if not line: # in case of empty line we remove selector as there is no data here anymore
@@ -160,6 +164,11 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
if key in ("PATH",) # whitelisted variables only
} | environment
result: dict[str, list[str]] = {
"stdout": [],
"stderr": [],
}
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, env=full_environment, text=True, encoding="utf8", errors="backslashreplace",
bufsize=1) as process:
@@ -168,38 +177,36 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
input_channel.write(input_data)
input_channel.close()
selector = selectors.DefaultSelector()
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
with selectors.DefaultSelector() as selector:
selector.register(get_io(process, "stdout"), selectors.EVENT_READ, data="stdout")
selector.register(get_io(process, "stderr"), selectors.EVENT_READ, data="stderr")
result: dict[str, list[str]] = {
"stdout": [],
"stderr": [],
}
while selector.get_map(): # while there are unread selectors, keep reading
for key_data, output in poll(selector):
result[key_data].append(output)
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
stderr = "\n".join(result["stderr"]).rstrip("\n")
while selector.get_map(): # while there are unread selectors, keep reading
for key_data, output in poll(selector):
result[key_data].append(output)
status_code = process.wait()
if status_code != 0:
if isinstance(exception, Exception):
raise exception
if callable(exception):
raise exception(status_code, list(args), stdout, stderr)
raise CalledProcessError(status_code, list(args), stderr)
return stdout
stdout = "\n".join(result["stdout"]).rstrip("\n") # remove newline at the end of any
stderr = "\n".join(result["stderr"]).rstrip("\n")
if status_code != 0:
if isinstance(exception, Exception):
raise exception
if callable(exception):
raise exception(status_code, list(args), stdout, stderr)
raise CalledProcessError(status_code, list(args), stderr)
return stdout
def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None:
def check_user(root: Path, *, unsafe: bool) -> None:
"""
check if current user is the owner of the root
Args:
paths(RepositoryPaths): repository paths object
root(Path): path to root directory (e.g. repository root
:attr:`ahriman.models.repository_paths.RepositoryPaths.root`)
unsafe(bool): if set no user check will be performed before path creation
Raises:
@@ -208,14 +215,16 @@ def check_user(paths: RepositoryPaths, *, unsafe: bool) -> None:
Examples:
Simply run function with arguments::
>>> check_user(paths, unsafe=False)
>>> check_user(root, unsafe=False)
"""
if not paths.root.exists():
if not root.exists():
return # no directory found, skip check
if unsafe:
return # unsafe flag is enabled, no check performed
current_uid = os.getuid()
root_uid, _ = paths.root_owner
current_uid = os.geteuid()
root_uid, _ = owner(root)
if current_uid != root_uid:
raise UnsafeRunError(current_uid, root_uid)
@@ -258,24 +267,22 @@ def extract_user() -> str | None:
@contextlib.contextmanager
def filelock(path: Path) -> Generator[None, None, None]:
def filelock(path: Path) -> Iterator[FileLock]:
"""
lock on file passed as argument
wrapper around :class:`filelock.FileLock`, which also removes locks afterward
Args:
path(Path): path object on which lock must be performed
path(Path): path to lock on. The lock file will be created as ``.{path.name}.lock``
Yields:
FileLock: acquired file lock instance
"""
lock_path = path.with_name(f".{path.name}")
lock_path = path.with_name(f".{path.name}.lock")
try:
with lock_path.open("ab") as lock_file:
fd = lock_file.fileno()
try:
fcntl.flock(fd, fcntl.LOCK_EX) # lock file and wait lock is until available
yield
finally:
fcntl.flock(fd, fcntl.LOCK_UN) # unlock file first
with FileLock(lock_path) as lock:
yield lock
finally:
lock_path.unlink(missing_ok=True) # remove lock file at the end
lock_path.unlink(missing_ok=True)
def filter_json(source: dict[str, Any], known_fields: Iterable[str]) -> dict[str, Any]:
@@ -318,6 +325,24 @@ def full_version(epoch: str | int | None, pkgver: str, pkgrel: str) -> str:
return f"{prefix}{pkgver}-{pkgrel}"
def list_flatmap(source: Iterable[T], extractor: Callable[[T], Iterable[R]]) -> list[R]:
"""
extract elements from list of lists, flatten them and apply ``extractor``
Args:
source(Iterable[T]): source list
extractor(Callable[[T], Iterable[R]]): property extractor
Returns:
list[R]: combined list of unique entries in properties list
"""
def generator() -> Iterator[R]:
for inner in source:
yield from extractor(inner)
return sorted(set(generator()))
def minmax(source: Iterable[T], *, key: Callable[[T], Any] | None = None) -> tuple[T, T]:
"""
get min and max value from iterable
@@ -334,6 +359,20 @@ def minmax(source: Iterable[T], *, key: Callable[[T], Any] | None = None) -> tup
return min(first_iter, key=key), max(second_iter, key=key) # type: ignore
def owner(path: Path) -> tuple[int, int]:
"""
retrieve owner information by path
Args:
path(Path): path for which extract ids
Returns:
tuple[int, int]: owner user and group ids of the directory
"""
stat = path.stat()
return stat.st_uid, stat.st_gid
def package_like(filename: Path) -> bool:
"""
check if file looks like package
@@ -517,6 +556,17 @@ def srcinfo_property_list(key: str, srcinfo: Mapping[str, Any], package_srcinfo:
return values
def symlink_relative(symlink: Path, source: Path) -> None:
"""
create symlink with relative path to the target directory
Args:
symlink(Path): path to symlink to create
source(Path): source file to be symlinked
"""
symlink.symlink_to(source.relative_to(symlink.parent, walk_up=True))
def trim_package(package_name: str) -> str:
"""
remove version bound and description from package name. Pacman allows to specify version bound (=, <=, >= etc.) for
@@ -544,7 +594,7 @@ def utcnow() -> datetime.datetime:
return datetime.datetime.now(datetime.UTC)
def walk(directory_path: Path) -> Generator[Path, None, None]:
def walk(directory_path: Path) -> Iterator[Path]:
"""
list all file paths in given directory

View File

@@ -17,23 +17,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# pylint: disable=too-many-lines,too-many-public-methods
from __future__ import annotations
import copy
from collections.abc import Callable, Generator, Iterable
from collections.abc import Iterable
from dataclasses import dataclass
from pathlib import Path
from pyalpm import vercmp # type: ignore[import-not-found]
from typing import Any, Self
from urllib.parse import urlparse
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR, Official, OfficialSyncdb
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.utils import dataclass_view, full_version, parse_version, srcinfo_property_list, utcnow
from ahriman.core.utils import dataclass_view, full_version, list_flatmap, parse_version, srcinfo_property_list
from ahriman.models.package_description import PackageDescription
from ahriman.models.package_source import PackageSource
from ahriman.models.pkgbuild import Pkgbuild
@@ -89,7 +84,7 @@ class Package(LazyLogging):
Returns:
list[str]: sum of dependencies per each package
"""
return self._package_list_property(lambda package: package.depends)
return list_flatmap(self.packages.values(), lambda package: package.depends)
@property
def depends_build(self) -> set[str]:
@@ -109,7 +104,7 @@ class Package(LazyLogging):
Returns:
list[str]: sum of test dependencies per each package
"""
return self._package_list_property(lambda package: package.check_depends)
return list_flatmap(self.packages.values(), lambda package: package.check_depends)
@property
def depends_make(self) -> list[str]:
@@ -119,7 +114,7 @@ class Package(LazyLogging):
Returns:
list[str]: sum of make dependencies per each package
"""
return self._package_list_property(lambda package: package.make_depends)
return list_flatmap(self.packages.values(), lambda package: package.make_depends)
@property
def depends_opt(self) -> list[str]:
@@ -129,7 +124,7 @@ class Package(LazyLogging):
Returns:
list[str]: sum of optional dependencies per each package
"""
return self._package_list_property(lambda package: package.opt_depends)
return list_flatmap(self.packages.values(), lambda package: package.opt_depends)
@property
def groups(self) -> list[str]:
@@ -139,7 +134,7 @@ class Package(LazyLogging):
Returns:
list[str]: sum of groups per each package
"""
return self._package_list_property(lambda package: package.groups)
return list_flatmap(self.packages.values(), lambda package: package.groups)
@property
def is_single_package(self) -> bool:
@@ -160,7 +155,7 @@ class Package(LazyLogging):
bool: ``True`` in case if package base looks like VCS package and ``False`` otherwise
"""
return self.base.endswith("-bzr") \
or self.base.endswith("-csv") \
or self.base.endswith("-cvs") \
or self.base.endswith("-darcs") \
or self.base.endswith("-git") \
or self.base.endswith("-hg") \
@@ -174,7 +169,7 @@ class Package(LazyLogging):
Returns:
list[str]: sum of licenses per each package
"""
return self._package_list_property(lambda package: package.licenses)
return list_flatmap(self.packages.values(), lambda package: package.licenses)
@property
def packages_full(self) -> list[str]:
@@ -205,7 +200,7 @@ class Package(LazyLogging):
package = pacman.handle.load_pkg(str(path))
description = PackageDescription.from_package(package, path)
return cls(
base=package.base,
base=package.base or package.name,
version=package.version,
remote=RemoteSource(source=PackageSource.Archive),
packages={package.name: description},
@@ -345,183 +340,6 @@ class Package(LazyLogging):
packager=packager,
)
@staticmethod
def local_files(path: Path) -> Generator[Path, None, None]:
"""
extract list of local files
Args:
path(Path): path to package sources directory
Yields:
Path: list of paths of files which belong to the package and distributed together with this tarball.
All paths are relative to the ``path``
Raises:
PackageInfoError: if there are parsing errors
"""
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
# we could use arch property, but for consistency it is better to call special method
architectures = Package.supported_architectures(path)
for architecture in architectures:
for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture):
if "::" in source:
_, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it
if urlparse(source).scheme:
# basically file schema should use absolute path which is impossible if we are distributing
# files together with PKGBUILD. In this case we are going to skip it also
continue
yield Path(source)
if (install := pkgbuild.get("install")) is not None:
yield Path(install)
@staticmethod
def supported_architectures(path: Path) -> set[str]:
"""
load supported architectures from package sources
Args:
path(Path): path to package sources directory
Returns:
set[str]: list of package supported architectures
"""
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
return set(pkgbuild.get("arch", []))
def _package_list_property(self, extractor: Callable[[PackageDescription], list[str]]) -> list[str]:
"""
extract list property from single packages and combine them into one list
Notes:
Basically this method is generic for type of ``list[T]``, but there is no trait ``Comparable`` in default
packages, thus we limit this method only to new types
Args:
extractor(Callable[[PackageDescription], list[str]): package property extractor
Returns:
list[str]: combined list of unique entries in properties list
"""
def generator() -> Generator[str, None, None]:
for package in self.packages.values():
yield from extractor(package)
return sorted(set(generator()))
def actual_version(self, configuration: Configuration) -> str:
"""
additional method to handle VCS package versions
Args:
configuration(Configuration): configuration instance
Returns:
str: package version if package is not VCS and current version according to VCS otherwise
"""
if not self.is_vcs:
return self.version
from ahriman.core.build_tools.task import Task
_, repository_id = configuration.check_loaded()
paths = configuration.repository_paths
task = Task(self, configuration, repository_id.architecture, paths)
try:
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
task.init(paths.cache_for(self.base), [], None)
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
except Exception:
self.logger.exception("cannot determine version of VCS package")
finally:
# clear log files generated by devtools
for log_file in paths.cache_for(self.base).glob("*.log"):
log_file.unlink()
return self.version
def full_depends(self, pacman: Pacman, packages: Iterable[Package]) -> list[str]:
"""
generate full dependencies list including transitive dependencies
Args:
pacman(Pacman): alpm wrapper instance
packages(Iterable[Package]): repository package list
Returns:
list[str]: all dependencies of the package
"""
dependencies = {}
# load own package dependencies
for package_base in packages:
for name, repo_package in package_base.packages.items():
dependencies[name] = repo_package.depends
for provides in repo_package.provides:
dependencies[provides] = repo_package.depends
# load repository dependencies
for database in pacman.handle.get_syncdbs():
for pacman_package in database.pkgcache:
dependencies[pacman_package.name] = pacman_package.depends
for provides in pacman_package.provides:
dependencies[provides] = pacman_package.depends
result = set(self.depends)
current_depends: set[str] = set()
while result != current_depends:
current_depends = copy.deepcopy(result)
for package in current_depends:
result.update(dependencies.get(package, []))
return sorted(result)
def is_newer_than(self, timestamp: float | int) -> bool:
"""
check if package was built after the specified timestamp
Args:
timestamp(float | int): timestamp to check build date against
Returns:
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
In case if build date is not set by any of packages, it returns False
"""
return any(
package.build_date > timestamp
for package in self.packages.values()
if package.build_date is not None
)
def is_outdated(self, remote: Package, configuration: Configuration, *,
calculate_version: bool = True) -> bool:
"""
check if package is out-of-dated
Args:
remote(Package): package properties from remote source
configuration(Configuration): configuration instance
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
(Default value = True)
Returns:
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
"""
vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
if calculate_version and not self.is_newer_than(min_vcs_build_date):
remote_version = remote.actual_version(configuration)
else:
remote_version = remote.version
return self.vercmp(remote_version) < 0
def next_pkgrel(self, local_version: str | None) -> str | None:
"""
generate next pkgrel variable. The package release will be incremented if ``local_version`` is more or equal to
@@ -557,7 +375,7 @@ class Package(LazyLogging):
Returns:
str: print-friendly string
"""
details = "" if self.is_single_package else f""" ({" ".join(sorted(self.packages.keys()))})"""
details = "" if self.is_single_package else f" ({" ".join(sorted(self.packages.keys()))})"
return f"{self.base}{details}"
def vercmp(self, version: str) -> int:
@@ -582,7 +400,7 @@ class Package(LazyLogging):
"""
return dataclass_view(self)
def with_packages(self, packages: list[Path], pacman: Pacman) -> None:
def with_packages(self, packages: Iterable[Path], pacman: Pacman) -> None:
"""
replace packages descriptions with ones from archives

View File

@@ -22,8 +22,10 @@ from dataclasses import dataclass
from io import StringIO
from pathlib import Path
from typing import Any, ClassVar, IO, Self
from urllib.parse import urlparse
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
from ahriman.core.utils import srcinfo_property_list
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@@ -103,6 +105,54 @@ class Pkgbuild(Mapping[str, Any]):
return cls({key: value for key, value in fields.items() if key})
@staticmethod
def local_files(path: Path) -> Iterator[Path]:
"""
extract list of local files
Args:
path(Path): path to package sources directory
Yields:
Path: list of paths of files which belong to the package and distributed together with this tarball.
All paths are relative to the ``path``
Raises:
PackageInfoError: if there are parsing errors
"""
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
# we could use arch property, but for consistency it is better to call special method
architectures = Pkgbuild.supported_architectures(path)
for architecture in architectures:
for source in srcinfo_property_list("source", pkgbuild, {}, architecture=architecture):
if "::" in source:
_, source = source.split("::", maxsplit=1) # in case if filename is specified, remove it
if urlparse(source).scheme:
# basically file schema should use absolute path which is impossible if we are distributing
# files together with PKGBUILD. In this case we are going to skip it also
continue
yield Path(source)
if (install := pkgbuild.get("install")) is not None:
yield Path(install)
@staticmethod
def supported_architectures(path: Path) -> set[str]:
"""
load supported architectures from package sources
Args:
path(Path): path to package sources directory
Returns:
set[str]: list of package supported architectures
"""
pkgbuild = Pkgbuild.from_file(path / "PKGBUILD")
return set(pkgbuild.get("arch", []))
def packages(self) -> dict[str, Self]:
"""
extract properties from internal package functions

View File

@@ -22,7 +22,7 @@ import shlex
from dataclasses import dataclass, fields
from pathlib import Path
from typing import Any, Generator, Self
from typing import Any, Iterator, Self
from ahriman.core.configuration.shell_template import ShellTemplate
from ahriman.core.utils import dataclass_view, filter_json
@@ -147,7 +147,7 @@ class PkgbuildPatch:
"""
if "$" in source:
# copy from library method with double quotes instead
return f"""\"{source.replace("\"", "'\"'")}\""""
return f"\"{source.replace("\"", "'\"'")}\""
# otherwise just return normal call
return shlex.quote(source)
@@ -166,7 +166,7 @@ class PkgbuildPatch:
ValueError: if no closing quotation
"""
def generator() -> Generator[str, None, None]:
def generator() -> Iterator[str]:
token = None
for char in source:
if token is not None:
@@ -195,13 +195,13 @@ class PkgbuildPatch:
"""
if isinstance(self.value, list): # list like
value = " ".join(map(self.quote, self.value))
return f"""{self.key}=({value})"""
return f"{self.key}=({value})"
if self.is_plain_diff: # no additional logic for plain diffs
return self.value
# we suppose that function values are only supported in string-like values
if self.is_function:
return f"{self.key} {self.value}" # no quoting enabled here
return f"""{self.key}={self.quote(self.value)}"""
return f"{self.key}={self.quote(self.value)}"
def substitute(self, variables: dict[str, str]) -> str | list[str]:
"""

View File

@@ -21,14 +21,14 @@ import contextlib
import os
import shutil
from collections.abc import Generator
from collections.abc import Iterator
from dataclasses import dataclass, field
from functools import cached_property
from pathlib import Path
from pwd import getpwuid
from ahriman.core.exceptions import PathError
from ahriman.core.log import LazyLogging
from ahriman.core.utils import owner
from ahriman.models.repository_id import RepositoryId
@@ -103,7 +103,7 @@ class RepositoryPaths(LazyLogging):
Returns:
Path: path to directory in which build process is run
"""
uid, _ = self.owner(self.root)
uid, _ = owner(self.root)
return self.chroot / f"{self.repository_id.name}-{self.repository_id.architecture}" / getpwuid(uid).pw_name
@property
@@ -165,7 +165,7 @@ class RepositoryPaths(LazyLogging):
Returns:
tuple[int, int]: owner user and group of the root directory
"""
return self.owner(self.root)
return owner(self.root)
# pylint: disable=protected-access
@classmethod
@@ -180,7 +180,7 @@ class RepositoryPaths(LazyLogging):
Returns:
set[str]: list of repository architectures for which there is created tree
"""
def walk(repository_dir: Path) -> Generator[str, None, None]:
def walk(repository_dir: Path) -> Iterator[str]:
for architecture in filter(lambda path: path.is_dir(), repository_dir.iterdir()):
yield architecture.name
@@ -207,7 +207,7 @@ class RepositoryPaths(LazyLogging):
is loaded in legacy mode
"""
# simply walk through the root. In case if there are subdirectories, emit the name
def walk(paths: RepositoryPaths) -> Generator[str, None, None]:
def walk(paths: RepositoryPaths) -> Iterator[str]:
for repository in filter(lambda path: path.is_dir(), paths._repository_root.iterdir()):
if any(path.is_dir() for path in repository.iterdir()):
yield repository.name
@@ -218,47 +218,6 @@ class RepositoryPaths(LazyLogging):
return set(walk(instance))
@staticmethod
def owner(path: Path) -> tuple[int, int]:
"""
retrieve owner information by path
Args:
path(Path): path for which extract ids
Returns:
tuple[int, int]: owner user and group ids of the directory
"""
stat = path.stat()
return stat.st_uid, stat.st_gid
def _chown(self, path: Path) -> None:
"""
set owner of path recursively (from root) to root owner
Notes:
More likely you don't want to call this method explicitly, consider using :func:`preserve_owner()`
as context manager instead
Args:
path(Path): path to be chown
Raises:
PathError: if path does not belong to root
"""
def set_owner(current: Path) -> None:
uid, gid = self.owner(current)
if uid == root_uid and gid == root_gid:
return
os.chown(current, root_uid, root_gid, follow_symlinks=False)
if self.root not in path.parents:
raise PathError(path, self.root)
root_uid, root_gid = self.root_owner
while path != self.root:
set_owner(path)
path = path.parent
def archive_for(self, package_base: str) -> Path:
"""
get path to archive specified search criteria
@@ -269,12 +228,7 @@ class RepositoryPaths(LazyLogging):
Returns:
Path: path to archive directory for package base
"""
directory = self.archive / "packages" / package_base[0] / package_base
if not directory.is_dir(): # create if not exists
with self.preserve_owner(self.archive):
directory.mkdir(mode=0o755, parents=True)
return directory
return self.archive / "packages" / package_base[0] / package_base
def cache_for(self, package_base: str) -> Path:
"""
@@ -288,13 +242,31 @@ class RepositoryPaths(LazyLogging):
"""
return self.cache / package_base
@contextlib.contextmanager
def preserve_owner(self, path: Path | None = None) -> Generator[None, None, None]:
def ensure_exists(self, directory: Path) -> Path:
"""
perform any action preserving owner for any newly created file or directory
get path based on ``directory`` callable provided and ensure it exists
Args:
path(Path | None, optional): use this path as root instead of repository root (Default value = None)
directory(Path): path to directory to check
Returns:
Path: original path based on extractor provided. Directory will always exist
Examples:
This method calls directory accessor and then checks if there is a directory and - otherwise - creates it::
>>> paths.ensure_exists(paths.archive_for(package_base))
"""
if not directory.is_dir():
with self.preserve_owner():
directory.mkdir(mode=0o755, parents=True)
return directory
@contextlib.contextmanager
def preserve_owner(self) -> Iterator[None]:
"""
perform any action preserving owner for any newly created file or directory
Examples:
This method is designed to use as context manager when you are going to perform operations which might
@@ -306,29 +278,26 @@ class RepositoryPaths(LazyLogging):
Note, however, that this method doesn't handle any exceptions and will eventually interrupt
if there will be any.
"""
path = path or self.root
# guard non-root
# the reason we do this is that it only works if permissions can be actually changed. Hence,
# non-privileged user (e.g. personal user or ahriman user) can't change permissions.
# The only one who can do so is root, so if user is not root we just terminate function
current_uid, current_gid = os.geteuid(), os.getegid()
if current_uid != 0:
yield
return
def walk(root: Path) -> Generator[Path, None, None]:
yield root
if not root.exists():
return
# set uid and gid to root owner
target_uid, target_gid = self.root_owner
os.setegid(target_gid)
os.seteuid(target_uid)
# basically walk, but skipping some content
for child in root.iterdir():
yield child
if child in (self.chroot.parent,):
yield from child.iterdir() # we only yield top-level in chroot directory
elif child.is_dir():
yield from walk(child)
# get current filesystem and run action
previous_snapshot = set(walk(path))
yield
# get newly created files and directories and chown them
new_entries = set(walk(path)).difference(previous_snapshot)
for entry in new_entries:
self._chown(entry)
try:
yield
finally:
# reset uid and gid
os.seteuid(current_uid)
os.setegid(current_gid)
def tree_clear(self, package_base: str) -> None:
"""
@@ -339,6 +308,7 @@ class RepositoryPaths(LazyLogging):
"""
for directory in (
self.cache_for(package_base),
self.archive_for(package_base),
):
shutil.rmtree(directory, ignore_errors=True)
@@ -349,13 +319,12 @@ class RepositoryPaths(LazyLogging):
if self.repository_id.is_empty:
return # do not even try to create tree in case if no repository id set
with self.preserve_owner():
for directory in (
self.archive,
self.cache,
self.chroot,
self.packages,
self.pacman,
self.repository,
):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
for directory in (
self.archive,
self.cache,
self.chroot,
self.packages,
self.pacman,
self.repository,
):
self.ensure_exists(directory)

View File

@@ -72,7 +72,7 @@ def _security() -> list[dict[str, Any]]:
return [{
"token": {
"type": "apiKey", # as per specification we are using api key
"name": "API_SESSION",
"name": "AHRIMAN",
"in": "cookie",
}
}]

View File

@@ -149,11 +149,17 @@ def setup_auth(application: Application, configuration: Configuration, validator
Application: configured web application
"""
secret_key = _cookie_secret_key(configuration)
storage = EncryptedCookieStorage(secret_key, cookie_name="API_SESSION", max_age=validator.max_age)
storage = EncryptedCookieStorage(
secret_key,
cookie_name="AHRIMAN",
max_age=validator.max_age,
httponly=True,
samesite="Lax",
)
setup_session(application, storage)
authorization_policy = _AuthorizationPolicy(validator)
identity_policy = aiohttp_security.SessionIdentityPolicy()
identity_policy = aiohttp_security.SessionIdentityPolicy("SESSION")
aiohttp_security.setup(application, identity_policy, authorization_policy)
application.middlewares.append(_auth_handler(validator.allow_read_only))

View File

@@ -20,7 +20,7 @@
import re
from aiohttp.web import Application, View
from collections.abc import Generator
from collections.abc import Iterator
import ahriman.web.views
@@ -32,7 +32,7 @@ from ahriman.web.views.base import BaseView
__all__ = ["setup_routes"]
def _dynamic_routes(configuration: Configuration) -> Generator[tuple[str, type[View]], None, None]:
def _dynamic_routes(configuration: Configuration) -> Iterator[tuple[str, type[View]]]:
"""
extract dynamic routes based on views

View File

@@ -25,6 +25,6 @@ class AuthSchema(Schema):
request cookie authorization schema
"""
API_SESSION = fields.String(required=True, metadata={
AHRIMAN = fields.String(required=True, metadata={
"description": "API session key as returned from authorization",
})

View File

@@ -28,3 +28,6 @@ class OAuth2Schema(Schema):
code = fields.String(metadata={
"description": "OAuth2 authorization code. In case if not set, the redirect to provider will be initiated",
})
state = fields.String(metadata={
"description": "CSRF token returned by OAuth2 provider",
})

View File

@@ -18,9 +18,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPBadRequest, HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized
from secrets import token_urlsafe
from typing import ClassVar
from ahriman.core.auth.helpers import remember
from ahriman.core.auth.helpers import get_session, remember
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import LoginSchema, OAuth2Schema
@@ -68,15 +69,18 @@ class LoginView(BaseView):
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
oauth_provider = self.validator
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
if not isinstance(oauth_provider, OAuth):
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
session = await get_session(self.request)
code = self.request.query.get("code")
if not code:
raise HTTPFound(oauth_provider.get_oauth_url())
state = session["state"] = token_urlsafe()
raise HTTPFound(oauth_provider.get_oauth_url(state))
response = HTTPFound("/")
identity = await oauth_provider.get_oauth_username(code)
identity = await oauth_provider.get_oauth_username(code, self.request.query.get("state"), session)
if identity is not None and await self.validator.known_username(identity):
await remember(self.request, response, identity)
raise response

View File

@@ -5,6 +5,7 @@ from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.application.application.application_repository import ApplicationRepository
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.tree import Leaf, Tree
from ahriman.models.changes import Changes
from ahriman.models.package import Package
@@ -135,7 +136,7 @@ def test_unknown_no_aur(application_repository: ApplicationRepository, package_a
must return empty list in case if there is locally stored PKGBUILD
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception)
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=UnknownPackageError(package_ahriman.base))
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False)
@@ -149,7 +150,7 @@ def test_unknown_no_aur_no_local(application_repository: ApplicationRepository,
must return list of packages missing in aur and in local storage
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception)
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=UnknownPackageError(package_ahriman.base))
mocker.patch("pathlib.Path.is_dir", return_value=False)
packages = application_repository.unknown()

View File

@@ -145,63 +145,11 @@ def test_repositories_extract(args: argparse.Namespace, configuration: Configura
args.configuration = configuration.path
args.repository = "repo"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("arch", "repo")])
assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
def test_repositories_extract_repository(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on flags and tree
"""
args.architecture = "arch"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value={"repo"})
assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_repository_legacy(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on flags and tree (legacy mode)
"""
args.architecture = "arch"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
assert Handler.repositories_extract(args) == [RepositoryId("arch", "aur")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_repositories_extract_architecture(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must read repository name from config
"""
args.configuration = configuration.path
args.repository = "repo"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures",
return_value={"arch"})
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Handler.repositories_extract(args) == [RepositoryId("arch", "repo")]
known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo")
known_repositories_mock.assert_not_called()
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), args.repository, args.architecture)
def test_repositories_extract_empty(args: argparse.Namespace, configuration: Configuration,
@@ -212,8 +160,7 @@ def test_repositories_extract_empty(args: argparse.Namespace, configuration: Con
args.command = "config"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set())
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", return_value=set())
mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=[])
with pytest.raises(MissingArchitectureError):
Handler.repositories_extract(args)
@@ -227,12 +174,11 @@ def test_repositories_extract_systemd(args: argparse.Namespace, configuration: C
args.configuration = configuration.path
args.repository_id = "i686/some/repo/name"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("i686", "some-repo-name")])
assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), "some-repo-name", "i686")
def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, configuration: Configuration,
@@ -243,12 +189,11 @@ def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, config
args.configuration = configuration.path
args.repository_id = "i686-some-repo-name"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("i686", "some-repo-name")])
assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), "some-repo-name", "i686")
def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configuration: Configuration,
@@ -259,10 +204,8 @@ def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configura
args.configuration = configuration.path
args.repository_id = "i686"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
extract_mock = mocker.patch("ahriman.core.repository.Explorer.repositories_extract",
return_value=[RepositoryId("i686", "aur")])
assert Handler.repositories_extract(args) == [RepositoryId("i686", "aur")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
extract_mock.assert_called_once_with(pytest.helpers.anyvar(Configuration, True), None, "i686")

View File

@@ -34,7 +34,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
_, repository_id = configuration.check_loaded()
Restore.run(args, repository_id, configuration, report=False)
extract_mock.extractall.assert_called_once_with(path=args.output)
extract_mock.extractall.assert_called_once_with(path=args.output, filter="data")
def test_disallow_multi_architecture_run() -> None:

View File

@@ -17,7 +17,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
"""
tree_create_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
application_mock = mocker.patch("ahriman.application.handlers.tree_migrate.TreeMigrate.tree_move")
symlinks_mock = mocker.patch("ahriman.application.handlers.tree_migrate.TreeMigrate.fix_symlinks")
symlinks_mock = mocker.patch("ahriman.application.handlers.tree_migrate.TreeMigrate.symlinks_fix")
_, repository_id = configuration.check_loaded()
old_paths = configuration.repository_paths
new_paths = RepositoryPaths(old_paths.root, old_paths.repository_id, _force_current_tree=True)
@@ -28,11 +28,10 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
symlinks_mock.assert_called_once_with(new_paths)
def test_fix_symlinks(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None:
def test_symlinks_fix(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must replace symlinks during migration
"""
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
mocker.patch("ahriman.application.handlers.tree_migrate.walk", side_effect=[
[
repository_paths.archive_for(package_ahriman.base) / "file",
@@ -47,7 +46,7 @@ def test_fix_symlinks(repository_paths: RepositoryPaths, package_ahriman: Packag
unlink_mock = mocker.patch("pathlib.Path.unlink")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
TreeMigrate.fix_symlinks(repository_paths)
TreeMigrate.symlinks_fix(repository_paths)
unlink_mock.assert_called_once_with()
symlink_mock.assert_called_once_with(
Path("..") /

View File

@@ -142,7 +142,7 @@ def test_check_user(lock: Lock, mocker: MockerFixture) -> None:
tree_create = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_create")
lock.check_user()
check_user_patch.assert_called_once_with(lock.paths, unsafe=False)
check_user_patch.assert_called_once_with(lock.paths.root, unsafe=False)
tree_create.assert_called_once_with()

View File

@@ -365,6 +365,27 @@ def package_python_schedule(
packages=packages)
@pytest.fixture
def package_tpacpi_bat_git() -> Package:
"""
git package fixture
Returns:
Package: git package test instance
"""
return Package(
base="tpacpi-bat-git",
version="3.1.r12.g4959b52-1",
remote=RemoteSource(
source=PackageSource.AUR,
git_url=AUR.remote_git_url("tpacpi-bat-git", "aur"),
web_url=AUR.remote_web_url("tpacpi-bat-git"),
path=".",
branch="master",
),
packages={"tpacpi-bat-git": PackageDescription()})
@pytest.fixture
def package_description_ahriman() -> PackageDescription:
"""

View File

@@ -67,7 +67,7 @@ def test_database_copy(pacman: Pacman, mocker: MockerFixture) -> None:
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True)
copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path)
owner_guard_mock.assert_called_once_with(dst_path.parent)
owner_guard_mock.assert_called_once_with()
def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:

View File

@@ -35,17 +35,6 @@ def test_repo_add(repo: Repo, mocker: MockerFixture) -> None:
assert "--remove" in check_output_mock.call_args[0]
def test_repo_add_no_remove(repo: Repo, mocker: MockerFixture) -> None:
"""
must call repo-add without remove flag
"""
check_output_mock = mocker.patch("ahriman.core.alpm.repo.check_output")
repo.add(Path("path"), remove=False)
check_output_mock.assert_called_once() # it will be checked later
assert "--remove" not in check_output_mock.call_args[0]
def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
"""
must call repo-add with empty package list on repo initializing
@@ -71,20 +60,6 @@ def test_repo_remove(repo: Repo, package_ahriman: Package, mocker: MockerFixture
assert package_ahriman.base in check_output_mock.call_args[0]
def test_repo_remove_guess_package(repo: Repo, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must call repo-remove on package removal if no package name set
"""
filepath = package_ahriman.packages[package_ahriman.base].filepath
mocker.patch("pathlib.Path.glob", return_value=[])
check_output_mock = mocker.patch("ahriman.core.alpm.repo.check_output")
repo.remove(None, filepath)
check_output_mock.assert_called_once() # it will be checked later
assert check_output_mock.call_args[0][0] == "repo-remove"
assert package_ahriman.base in check_output_mock.call_args[0]
def test_repo_remove_fail_no_file(repo: Repo, mocker: MockerFixture) -> None:
"""
must fail removal on missing file

View File

@@ -7,6 +7,18 @@ from ahriman.core.utils import utcnow
from ahriman.models.package import Package
def test_repo(archive_tree: ArchiveTree) -> None:
"""
must return correct repository object
"""
local = Path("local")
repo = archive_tree._repo(local)
assert repo.sign_args == archive_tree.sign_args
assert repo.name == archive_tree.repository_id.name
assert repo.root == local
def test_repository_for(archive_tree: ArchiveTree) -> None:
"""
must correctly generate path to repository
@@ -17,6 +29,25 @@ def test_repository_for(archive_tree: ArchiveTree) -> None:
assert set(map("{:02d}".format, utcnow().timetuple()[:3])).issubset(path.parts)
def test_directories_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
"""
must remove empty directories recursively
"""
root = archive_tree.paths.archive / "repos"
(root / "a" / "b").mkdir(parents=True, exist_ok=True)
(root / "a" / "b" / "file").touch()
(root / "a" / "b" / "c" / "d").mkdir(parents=True, exist_ok=True)
_original_rmdir = Path.rmdir
rmdir_mock = mocker.patch("pathlib.Path.rmdir", autospec=True, side_effect=_original_rmdir)
archive_tree.directories_fix({Path("a") / "b" / "c" / "d"})
rmdir_mock.assert_has_calls([
MockCall(root / "a" / "b" / "c" / "d"),
MockCall(root / "a" / "b" / "c"),
])
def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, package_python_schedule: Package,
mocker: MockerFixture) -> None:
"""
@@ -40,7 +71,7 @@ def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, pa
.relative_to(archive_tree.paths.root)
.relative_to("archive") /
single.filename
)
)
for package in (package_ahriman, package_python_schedule)
for single in package.packages.values()
])
@@ -68,7 +99,7 @@ def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
_original_exists = Path.exists
def exists_mock(path: Path) -> bool:
if path.name == "symlink":
if path.name.startswith("symlink"):
return True
return _original_exists(path)
@@ -76,13 +107,21 @@ def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
walk_mock = mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
archive_tree.repository_for() / filename
for filename in ("symlink", "broken_symlink", "file")
for filename in (
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
"symlink-1.0.0-1-x86_64.pkg.tar.zst.sig",
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
"file-1.0.0-1-x86_64.pkg.tar.zst",
)
])
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
archive_tree.symlinks_fix()
assert list(archive_tree.symlinks_fix()) == [
archive_tree.repository_for().relative_to(archive_tree.paths.archive / "repos"),
]
walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos")
remove_mock.assert_called_once_with(None, archive_tree.repository_for() / "broken_symlink")
remove_mock.assert_called_once_with(
"broken_symlink", archive_tree.repository_for() / "broken_symlink-1.0.0-1-x86_64.pkg.tar.zst")
def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
@@ -92,7 +131,7 @@ def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: Mock
_original_exists = Path.exists
def exists_mock(path: Path) -> bool:
if path.name == "symlink":
if path.name.startswith("symlink"):
return True
return _original_exists(path)
@@ -100,11 +139,15 @@ def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: Mock
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
archive_tree.repository_for().with_name("i686") / filename
for filename in ("symlink", "broken_symlink", "file")
for filename in (
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
"file-1.0.0-1-x86_64.pkg.tar.zst",
)
])
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
archive_tree.symlinks_fix()
assert list(archive_tree.symlinks_fix()) == []
remove_mock.assert_not_called()
@@ -117,7 +160,7 @@ def test_tree_create(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
archive_tree.tree_create()
owner_guard_mock.assert_called_once_with(archive_tree.paths.archive)
owner_guard_mock.assert_called_once_with()
mkdir_mock.assert_called_once_with(0o755, parents=True)
init_mock.assert_called_once_with()

View File

@@ -1,3 +1,4 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.archive import ArchiveTrigger
@@ -27,6 +28,10 @@ def test_on_stop(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None
"""
must fix broken symlinks on stop
"""
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix")
local = Path("local")
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix", return_value=[local])
directories_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.directories_fix")
archive_trigger.on_stop()
symlinks_mock.assert_called_once_with()
directories_mock.assert_called_once_with({local})

View File

@@ -13,6 +13,13 @@ def test_import_aiohttp_security() -> None:
assert helpers.aiohttp_security
def test_import_aiohttp_session() -> None:
"""
must import aiohttp_session correctly
"""
assert helpers.aiohttp_session
async def test_authorized_userid_dummy(mocker: MockerFixture) -> None:
"""
must not call authorized_userid from library if not enabled
@@ -55,6 +62,23 @@ async def test_forget_dummy(mocker: MockerFixture) -> None:
await helpers.forget()
async def test_get_session_dummy(mocker: MockerFixture) -> None:
"""
must return empty dict if no aiohttp_session module found
"""
mocker.patch.object(helpers, "aiohttp_session", None)
assert await helpers.get_session() == {}
async def test_get_session_library(mocker: MockerFixture) -> None:
"""
must call get_session from library if enabled
"""
get_session_mock = mocker.patch("aiohttp_session.get_session")
await helpers.get_session()
get_session_mock.assert_called_once_with()
async def test_forget_library(mocker: MockerFixture) -> None:
"""
must call forget from library if enabled
@@ -88,3 +112,12 @@ def test_import_aiohttp_security_missing(mocker: MockerFixture) -> None:
mocker.patch.dict(sys.modules, {"aiohttp_security": None})
importlib.reload(helpers)
assert helpers.aiohttp_security is None
def test_import_aiohttp_session_missing(mocker: MockerFixture) -> None:
"""
must set missing flag if no aiohttp_session module found
"""
mocker.patch.dict(sys.modules, {"aiohttp_session": None})
importlib.reload(helpers)
assert helpers.aiohttp_session is None

View File

@@ -57,8 +57,8 @@ def test_get_oauth_url(oauth: OAuth, mocker: MockerFixture) -> None:
must generate valid OAuth authorization URL
"""
authorize_url_mock = mocker.patch("aioauth_client.GoogleClient.get_authorize_url")
oauth.get_oauth_url()
authorize_url_mock.assert_called_once_with(scope=oauth.scopes, redirect_uri=oauth.redirect_uri)
oauth.get_oauth_url(state="state")
authorize_url_mock.assert_called_once_with(scope=oauth.scopes, redirect_uri=oauth.redirect_uri, state="state")
async def test_get_oauth_username(oauth: OAuth, mocker: MockerFixture) -> None:
@@ -69,10 +69,9 @@ async def test_get_oauth_username(oauth: OAuth, mocker: MockerFixture) -> None:
user_info_mock = mocker.patch("aioauth_client.GoogleClient.user_info",
return_value=(aioauth_client.User(email="email"), ""))
email = await oauth.get_oauth_username("code")
assert await oauth.get_oauth_username("code", state="state", session={"state": "state"}) == "email"
access_token_mock.assert_called_once_with("code", redirect_uri=oauth.redirect_uri)
user_info_mock.assert_called_once_with()
assert email == "email"
async def test_get_oauth_username_empty_email(oauth: OAuth, mocker: MockerFixture) -> None:
@@ -82,8 +81,7 @@ async def test_get_oauth_username_empty_email(oauth: OAuth, mocker: MockerFixtur
mocker.patch("aioauth_client.GoogleClient.get_access_token", return_value=("token", ""))
mocker.patch("aioauth_client.GoogleClient.user_info", return_value=(aioauth_client.User(username="username"), ""))
username = await oauth.get_oauth_username("code")
assert username == "username"
assert await oauth.get_oauth_username("code", state="state", session={"state": "state"}) == "username"
async def test_get_oauth_username_exception_1(oauth: OAuth, mocker: MockerFixture) -> None:
@@ -93,8 +91,7 @@ async def test_get_oauth_username_exception_1(oauth: OAuth, mocker: MockerFixtur
mocker.patch("aioauth_client.GoogleClient.get_access_token", side_effect=Exception)
user_info_mock = mocker.patch("aioauth_client.GoogleClient.user_info")
email = await oauth.get_oauth_username("code")
assert email is None
assert await oauth.get_oauth_username("code", state="state", session={"state": "state"}) is None
user_info_mock.assert_not_called()
@@ -105,5 +102,19 @@ async def test_get_oauth_username_exception_2(oauth: OAuth, mocker: MockerFixtur
mocker.patch("aioauth_client.GoogleClient.get_access_token", return_value=("token", ""))
mocker.patch("aioauth_client.GoogleClient.user_info", side_effect=Exception)
email = await oauth.get_oauth_username("code")
assert email is None
username = await oauth.get_oauth_username("code", state="state", session={"state": "state"})
assert username is None
async def test_get_oauth_username_csrf_missing(oauth: OAuth) -> None:
"""
must return None if CSRF state is missing
"""
assert await oauth.get_oauth_username("code", state=None, session={"state": "state"}) is None
async def test_get_oauth_username_csrf_mismatch(oauth: OAuth) -> None:
"""
must return None if CSRF state does not match session
"""
assert await oauth.get_oauth_username("code", state="wrong", session={"state": "state"}) is None

View File

@@ -0,0 +1,111 @@
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.core.build_tools.package_version import PackageVersion
from ahriman.core.configuration import Configuration
from ahriman.core.utils import utcnow
from ahriman.models.package import Package
from ahriman.models.pkgbuild import Pkgbuild
def test_actual_version(package_ahriman: Package, configuration: Configuration) -> None:
"""
must return same actual_version as version is
"""
assert PackageVersion(package_ahriman).actual_version(configuration) == package_ahriman.version
def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Configuration,
mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must return valid actual_version for VCS package
"""
pkgbuild = resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild"
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild))
mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
unlink_mock = mocker.patch("pathlib.Path.unlink")
assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == "3.1.r13.g4959b52-1"
init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None)
unlink_mock.assert_called_once_with()
def test_actual_version_failed(package_tpacpi_bat_git: Package, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must return same version in case if exception occurred
"""
mocker.patch("ahriman.core.build_tools.task.Task.init", side_effect=Exception)
mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
unlink_mock = mocker.patch("pathlib.Path.unlink")
assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == package_tpacpi_bat_git.version
unlink_mock.assert_called_once_with()
def test_is_newer_than(package_ahriman: Package, package_python_schedule: Package) -> None:
"""
must correctly check if package is newer than specified timestamp
"""
# base checks, true/false
older = package_ahriman.packages[package_ahriman.base].build_date - 1
assert PackageVersion(package_ahriman).is_newer_than(older)
newer = package_ahriman.packages[package_ahriman.base].build_date + 1
assert not PackageVersion(package_ahriman).is_newer_than(newer)
# list check
min_date = min(package.build_date for package in package_python_schedule.packages.values())
assert PackageVersion(package_python_schedule).is_newer_than(min_date)
# null list check
package_python_schedule.packages["python-schedule"].build_date = None
assert PackageVersion(package_python_schedule).is_newer_than(min_date)
package_python_schedule.packages["python2-schedule"].build_date = None
assert not PackageVersion(package_python_schedule).is_newer_than(min_date)
def test_is_outdated_false(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must be not outdated for the same package
"""
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version",
return_value=package_ahriman.version)
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration)
actual_version_mock.assert_called_once_with(configuration)
def test_is_outdated_true(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must be outdated for the new version
"""
other = Package.from_json(package_ahriman.view())
other.version = other.version.replace("-1", "-2")
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version",
return_value=other.version)
assert PackageVersion(package_ahriman).is_outdated(other, configuration)
actual_version_mock.assert_called_once_with(configuration)
def test_is_outdated_no_version_calculation(package_ahriman: Package, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must not call actual version if calculation is disabled
"""
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version")
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration, calculate_version=False)
actual_version_mock.assert_not_called()
def test_is_outdated_fresh_package(package_ahriman: Package, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must not call actual version if package is never than specified time
"""
configuration.set_option("build", "vcs_allowed_age", str(int(utcnow().timestamp())))
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version")
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration)
actual_version_mock.assert_not_called()

View File

@@ -55,7 +55,8 @@ def test_extend_architectures(mocker: MockerFixture) -> None:
must update available architecture list
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
architectures_mock = mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"x86_64"})
architectures_mock = mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures",
return_value={"x86_64"})
assert Sources.extend_architectures(Path("local"), "i686") == [PkgbuildPatch("arch", list({"x86_64", "i686"}))]
architectures_mock.assert_called_once_with(Path("local"))
@@ -66,7 +67,7 @@ def test_extend_architectures_any(mocker: MockerFixture) -> None:
must skip architecture patching in case if there is any architecture
"""
mocker.patch("pathlib.Path.is_file", return_value=True)
mocker.patch("ahriman.models.package.Package.supported_architectures", return_value={"any"})
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.supported_architectures", return_value={"any"})
assert Sources.extend_architectures(Path("local"), "i686") == []
@@ -191,7 +192,7 @@ def test_init(sources: Sources, mocker: MockerFixture) -> None:
"""
must create empty repository at the specified path
"""
mocker.patch("ahriman.models.package.Package.local_files", return_value=[Path("local")])
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.local_files", return_value=[Path("local")])
mocker.patch("pathlib.Path.is_dir", return_value=False)
add_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.add")
check_output_mock = mocker.patch("ahriman.core.build_tools.sources.check_output")
@@ -209,7 +210,7 @@ def test_init_skip(mocker: MockerFixture) -> None:
"""
must skip git init if it was already
"""
mocker.patch("ahriman.models.package.Package.local_files", return_value=[Path("local")])
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.local_files", return_value=[Path("local")])
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.build_tools.sources.Sources.add")
mocker.patch("ahriman.core.build_tools.sources.Sources.commit")

View File

@@ -33,6 +33,14 @@ def test_normalize_coerce_boolean(validator: Validator, mocker: MockerFixture) -
convert_mock.assert_called_once_with("1")
def test_normalize_coerce_float(validator: Validator) -> None:
"""
must convert string value to float by using configuration converters
"""
assert validator._normalize_coerce_float("1.5") == 1.5
assert validator._normalize_coerce_float("0.0") == 0.0
def test_normalize_coerce_integer(validator: Validator) -> None:
"""
must convert string value to integer by using configuration converters

View File

@@ -23,7 +23,7 @@ def test_migrate_data(connection: Connection, configuration: Configuration, mock
repository_id,
replace(repository_id, architecture="i686"),
]
mocker.patch("ahriman.application.handlers.handler.Handler.repositories_extract", return_value=repositories)
mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=repositories)
migration_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.move_packages")
migrate_data(connection, configuration)
@@ -47,22 +47,24 @@ def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, packag
repository_paths.repository / "directory",
repository_paths.repository / "file.pkg.tar.xz",
repository_paths.repository / "file.pkg.tar.xz.sig",
repository_paths.repository / "file2.pkg.tar.xz",
repository_paths.repository / "symlink.pkg.tar.xz",
])
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=is_file)
mocker.patch("pathlib.Path.exists", return_value=True)
archive_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
rename_mock = mocker.patch("pathlib.Path.rename")
move_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.atomic_move")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
move_packages(repository_paths, pacman)
archive_mock.assert_has_calls([
MockCall(repository_paths.repository / "file.pkg.tar.xz", pacman),
MockCall(repository_paths.repository / "file.pkg.tar.xz.sig", pacman),
MockCall(repository_paths.repository / filename, pacman)
for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz")
])
rename_mock.assert_has_calls([
MockCall(repository_paths.archive_for(package_ahriman.base) / "file.pkg.tar.xz"),
MockCall(repository_paths.archive_for(package_ahriman.base) / "file.pkg.tar.xz.sig"),
move_mock.assert_has_calls([
MockCall(repository_paths.repository / filename, repository_paths.archive_for(package_ahriman.base) / filename)
for filename in ("file.pkg.tar.xz", "file.pkg.tar.xz.sig", "file2.pkg.tar.xz")
])
symlink_mock.assert_has_calls([
MockCall(
@@ -70,13 +72,7 @@ def test_move_packages(repository_paths: RepositoryPaths, pacman: Pacman, packag
".." /
".." /
repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) /
"file.pkg.tar.xz"
),
MockCall(
Path("..") /
".." /
".." /
repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) /
"file.pkg.tar.xz.sig"
),
filename
)
for filename in ("file.pkg.tar.xz", "file.pkg.tar.xz.sig", "file2.pkg.tar.xz")
])

View File

@@ -16,7 +16,7 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
init_mock.assert_called_once_with()
def test_init(database: SQLite, configuration: Configuration, mocker: MockerFixture) -> None:
def test_init(database: SQLite, mocker: MockerFixture) -> None:
"""
must run migrations on init
"""

View File

@@ -1,6 +1,5 @@
import pytest
import requests
import requests_unixsocket
from pytest_mock import MockerFixture
@@ -8,31 +7,32 @@ from ahriman.core.http import SyncAhrimanClient
from ahriman.models.user import User
def test_session(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
def test_adapters(ahriman_client: SyncAhrimanClient) -> None:
"""
must create normal requests session
must return native adapters
"""
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
assert isinstance(ahriman_client.session, requests.Session)
assert not isinstance(ahriman_client.session, requests_unixsocket.Session)
login_mock.assert_called_once_with(pytest.helpers.anyvar(int))
assert "http+unix://" not in ahriman_client.adapters()
def test_session_unix_socket(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
def test_adapters_unix_socket(ahriman_client: SyncAhrimanClient) -> None:
"""
must create unix socket session
must register unix socket adapter
"""
login_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient._login")
ahriman_client.address = "http+unix://path"
assert isinstance(ahriman_client.session, requests_unixsocket.Session)
login_mock.assert_not_called()
assert "http+unix://" in ahriman_client.adapters()
def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
"""
must login user
must generate login url correctly
"""
assert ahriman_client._login_url().startswith(ahriman_client.address)
assert ahriman_client._login_url().endswith("/api/v1/login")
def test_on_session_creation(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must log in user on start
"""
ahriman_client.auth = (user.username, user.password)
requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request")
@@ -42,40 +42,32 @@ def test_login(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixt
}
session = requests.Session()
ahriman_client._login(session)
ahriman_client.on_session_creation(session)
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
def test_login_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
def test_on_session_creation_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must suppress any exception happened during login
must suppress any exception happened during session start
"""
ahriman_client.user = user
mocker.patch("requests.Session.request", side_effect=Exception)
ahriman_client._login(requests.Session())
ahriman_client.on_session_creation(requests.Session())
def test_login_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
def test_start_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
"""
must suppress HTTP exception happened during login
must suppress HTTP exception happened during session start
"""
ahriman_client.user = user
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
ahriman_client._login(requests.Session())
ahriman_client.on_session_creation(requests.Session())
def test_login_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
def test_start_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
"""
must skip login if no user set
"""
requests_mock = mocker.patch("requests.Session.request")
ahriman_client._login(requests.Session())
ahriman_client.on_session_creation(requests.Session())
requests_mock.assert_not_called()
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
"""
must generate login url correctly
"""
assert ahriman_client._login_url().startswith(ahriman_client.address)
assert ahriman_client._login_url().endswith("/api/v1/login")

View File

@@ -4,6 +4,7 @@ import requests
from pytest_mock import MockerFixture
from unittest.mock import MagicMock, call as MockCall
from ahriman.core.alpm.remote import AUR
from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient
@@ -33,12 +34,33 @@ def test_init_auth_empty() -> None:
assert SyncHttpClient().auth is None
def test_session() -> None:
def test_session(mocker: MockerFixture) -> None:
"""
must generate valid session
"""
on_session_creation_mock = mocker.patch("ahriman.core.http.sync_http_client.SyncHttpClient.on_session_creation")
session = SyncHttpClient().session
assert "User-Agent" in session.headers
on_session_creation_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_retry_policy() -> None:
"""
must set retry policy
"""
SyncHttpClient.DEFAULT_MAX_RETRIES = 1
SyncHttpClient.DEFAULT_RETRY_BACKOFF = 2.0
AUR.DEFAULT_MAX_RETRIES = 3
AUR.DEFAULT_RETRY_BACKOFF = 4.0
client = SyncHttpClient()
assert client.retry.connect == 1
assert client.retry.backoff_factor == 2.0
aur = AUR()
assert aur.retry.connect == 3
assert aur.retry.backoff_factor == 4.0
def test_exception_response_text() -> None:
@@ -60,6 +82,18 @@ def test_exception_response_text_empty() -> None:
assert SyncHttpClient.exception_response_text(exception) == ""
def test_adapters() -> None:
"""
must create adapters with retry policy
"""
client = SyncHttpClient()
adapters = client.adapters()
assert "http://" in adapters
assert "https://" in adapters
assert all(adapter.max_retries == client.retry for adapter in adapters.values())
def test_make_request(mocker: MockerFixture) -> None:
"""
must make HTTP request
@@ -158,3 +192,11 @@ def test_make_request_session() -> None:
session_mock.request.assert_called_once_with(
"GET", "url", params=None, data=None, headers=None, files=None, json=None,
stream=None, auth=None, timeout=client.timeout)
def test_on_session_creation() -> None:
"""
must do nothing on start
"""
client = SyncHttpClient()
client.on_session_creation(client.session)

View File

@@ -2,7 +2,6 @@ import logging
import pytest
from pytest_mock import MockerFixture
from unittest.mock import call as MockCall
from ahriman.core.alpm.repo import Repo
from ahriman.core.build_tools.task import Task

View File

@@ -1,3 +1,5 @@
import smtplib
import pytest
from pytest_mock import MockerFixture
@@ -6,6 +8,7 @@ from ahriman.core.configuration import Configuration
from ahriman.core.report.email import Email
from ahriman.models.package import Package
from ahriman.models.result import Result
from ahriman.models.smtp_ssl_settings import SmtpSSLSettings
def test_template(configuration: Configuration) -> None:
@@ -37,17 +40,36 @@ def test_template_full(configuration: Configuration) -> None:
assert Email(repository_id, configuration, "email").template_full == root.parent / template
def test_smtp_session(email: Email) -> None:
"""
must build normal SMTP session if SSL is disabled
"""
email.ssl = SmtpSSLSettings.Disabled
assert email._smtp_session == smtplib.SMTP
email.ssl = SmtpSSLSettings.STARTTLS
assert email._smtp_session == smtplib.SMTP
def test_smtp_session_ssl(email: Email) -> None:
"""
must build SMTP_SSL session if SSL is enabled
"""
email.ssl = SmtpSSLSettings.SSL
assert email._smtp_session == smtplib.SMTP_SSL
def test_send(email: Email, mocker: MockerFixture) -> None:
"""
must send an email with attachment
"""
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
email._send("a text", {"attachment.html": "an attachment"})
smtp_mock.return_value.starttls.assert_not_called()
smtp_mock.return_value.login.assert_not_called()
smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int))
smtp_mock.return_value.quit.assert_called_once_with()
def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
@@ -57,6 +79,7 @@ def test_send_auth(configuration: Configuration, mocker: MockerFixture) -> None:
configuration.set_option("email", "user", "username")
configuration.set_option("email", "password", "password")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -70,6 +93,7 @@ def test_send_auth_no_password(configuration: Configuration, mocker: MockerFixtu
"""
configuration.set_option("email", "user", "username")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -83,6 +107,7 @@ def test_send_auth_no_user(configuration: Configuration, mocker: MockerFixture)
"""
configuration.set_option("email", "password", "password")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -96,6 +121,7 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
"""
configuration.set_option("email", "ssl", "ssl")
smtp_mock = mocker.patch("smtplib.SMTP_SSL")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")
@@ -103,7 +129,6 @@ def test_send_ssl_tls(configuration: Configuration, mocker: MockerFixture) -> No
smtp_mock.return_value.starttls.assert_not_called()
smtp_mock.return_value.login.assert_not_called()
smtp_mock.return_value.sendmail.assert_called_once_with(email.sender, email.receivers, pytest.helpers.anyvar(int))
smtp_mock.return_value.quit.assert_called_once_with()
def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> None:
@@ -112,6 +137,7 @@ def test_send_starttls(configuration: Configuration, mocker: MockerFixture) -> N
"""
configuration.set_option("email", "ssl", "starttls")
smtp_mock = mocker.patch("smtplib.SMTP")
smtp_mock.return_value.__enter__.return_value = smtp_mock.return_value
_, repository_id = configuration.check_loaded()
email = Email(repository_id, configuration, "email")

Some files were not shown because too many files have changed in this diff Show More