Compare commits

...

19 Commits

Author SHA1 Message Date
4b8177ae6a Release 2.19.2 2026-01-25 10:43:06 +02:00
f13a2fde85 type: use as keyword in case match 2026-01-25 10:39:15 +02:00
847c029c46 fix: correctly process trigger repo specific settings in validator (see #154) 2026-01-25 10:24:01 +02:00
a647783252 feat: expose repository name and architecure in configuration if available
In some cases there are reference to current repository settings. In
order to handle it correctly two ro options have been added

Related to #154
2026-01-25 10:23:30 +02:00
5cfcb5c3e8 fix: careful handling of file permissions during initialization
It has been found that during cold start (e.g. in docker container),
some permissions are invalid. In order to handle that, some operations
are not guarded with RepositoryPaths.preserve_root guard

In addition, it has been also found that in some cases (e.g. web server
start) migrations are performed on empty repository identifier which may
lead to wrong data (see also 435375721d),
as well as some unexpected results during database operations. In order
to handle that, now all watcher instances have their own databases (and
configurations)
2026-01-25 10:20:29 +02:00
6280c9dbe6 Release 2.19.1 2025-07-14 21:41:02 +03:00
3d1fdd5517 fix: fix migrations on empty repositories 2025-07-14 21:37:43 +03:00
ab022071e8 fix: trim provides/depends versions and lookup provides through pkgname
(#150)

Current implementation did it in wrong way. First of all, there was a
lookup through pkgbase instead of pkgname, which lead to errors, because
aur api doesn't allow to search by pkgbase (as well as provides is
basically pkgname instead)

It also was found that dependencies resolution lookup has been performed
by using raw packages array, which can include versions, descriptions
etc
2025-07-14 21:37:35 +03:00
a01f76df42 fix: separate ua by spaces 2025-07-14 21:37:25 +03:00
2b1b17a1a3 Release 2.19.0 2025-06-29 03:00:41 +03:00
9e6705056a build: use archlinux images for release build 2025-06-29 02:59:28 +03:00
b3a3a81f70 feat: add ability to refresh databases through web interface 2025-06-29 02:44:57 +03:00
3e5dbbd6cd feat: extend user-agent 2025-06-28 23:08:31 +03:00
f41e44895d fix: support provides in aur (#146)
* support provides in aur

* process provides during tree resolution

* stylish
2025-06-28 22:39:54 +03:00
765bbf486f feat: port to new AUR API 2025-06-28 22:07:59 +03:00
a3c54afb82 fix: process unicode errors in command execution 2025-06-28 20:26:47 +03:00
7f223ecc0a docs: extract version for the manpage 2025-06-25 02:14:57 +03:00
7769a4a6e0 Release 2.18.3 2025-06-20 17:20:19 +03:00
066d1b1dde refactor: rework few tests and build system
This commit includes the following changes
* Bump github actions
* Update tests github action to check documentation and streamline
  process
* Update test cases to use temporary directories as roots
* Simplify tox.ini
2025-06-20 17:04:57 +03:00
84 changed files with 2861 additions and 1715 deletions

View File

@@ -1,6 +0,0 @@
skips:
- B101
- B104
- B105
- B106
- B404

View File

@@ -21,18 +21,18 @@ jobs:
packages: write
steps:
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
- name: Login to docker hub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to github container registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -40,7 +40,7 @@ jobs:
- name: Extract docker metadata
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
images: |
arcan1s/ahriman
@@ -50,7 +50,7 @@ jobs:
type=edge
- name: Build an image and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v6
with:
file: docker/Dockerfile
push: true

View File

@@ -37,8 +37,6 @@ jobs:
- repo:/var/lib/ahriman
steps:
- uses: actions/checkout@v3
- run: pacman -Sy
- name: Init repository

View File

@@ -13,8 +13,16 @@ jobs:
runs-on: ubuntu-latest
container:
image: archlinux:base
options: -w /build
volumes:
- ${{ github.workspace }}:/build
steps:
- uses: actions/checkout@v3
- run: pacman --noconfirm -Syu base-devel git python-tox
- uses: actions/checkout@v4
- name: Extract version
id: version
@@ -27,18 +35,13 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+'
- name: Install dependencies
uses: ConorMacBride/install-package@v1.1.0
with:
apt: tox
- name: Create archive
run: tox -e archive
env:
VERSION: ${{ steps.version.outputs.VERSION }}
- name: Publish release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
body: |
${{ steps.changelog.outputs.compareurl }}

View File

@@ -24,7 +24,7 @@ jobs:
- ${{ github.workspace }}:/build
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup the minimal service in arch linux container
run: .github/workflows/setup.sh minimal
@@ -40,7 +40,7 @@ jobs:
options: --privileged -w /build
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup the service in arch linux container
run: .github/workflows/setup.sh

View File

@@ -1,10 +0,0 @@
#!/bin/bash
# Install dependencies and run test in container
set -ex
# install dependencies
pacman --noconfirm -Syyu base-devel python-tox
# run test and check targets
tox

View File

@@ -26,7 +26,16 @@ jobs:
- ${{ github.workspace }}:/build
steps:
- uses: actions/checkout@v3
- run: pacman --noconfirm -Syu base-devel git python-tox
- name: Run check and tests in arch linux container
run: .github/workflows/tests.sh
- run: git config --global --add safe.directory *
- uses: actions/checkout@v4
- name: Run check and tests
run: tox
- name: Generate documentation and check if there are untracked changes
run: |
tox -e docs
[ -z "$(git status --porcelain docs/*.rst)" ]

View File

@@ -1,10 +1,10 @@
[tool.pylint.main]
init-hook = "sys.path.append('pylint_plugins')"
init-hook = "sys.path.append('tools')"
load-plugins = [
"pylint.extensions.docparams",
"pylint.extensions.bad_builtin",
"definition_order",
"import_order",
"pylint_plugins.definition_order",
"pylint_plugins.import_order",
]
[tool.pylint.classes]

5
.pytest.ini Normal file
View File

@@ -0,0 +1,5 @@
[pytest]
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
asyncio_default_fixture_loop_scope = function
asyncio_mode = auto
spec_test_format = {result} {docstring_summary}

File diff suppressed because it is too large Load Diff

View File

@@ -413,10 +413,11 @@ Web application
Web application requires the following python packages to be installed:
* Core part requires ``aiohttp`` (application itself), ``aiohttp_jinja2`` and ``Jinja2`` (HTML generation from templates).
* Additional web features also require ``aiohttp-apispec`` (autogenerated documentation), ``aiohttp_cors`` (CORS support, required by documentation).
* Additional web features also require ``aiohttp-apispec`` (autogenerated documentation, optional), ``aiohttp_cors`` (CORS support, required by documentation).
* In addition, authorization feature requires ``aiohttp_security``, ``aiohttp_session`` and ``cryptography``.
* In addition to base authorization dependencies, OAuth2 also requires ``aioauth-client`` library.
* In addition if you would like to disable authorization for local access (recommended way in order to run the application itself with reporting support), the ``requests-unixsocket2`` library is required.
* Application metrics will be automatically enabled after installing ``aiohttp-openmetrics`` package.
Middlewares
^^^^^^^^^^^

View File

@@ -138,6 +138,8 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
Base repository settings.
* ``architecture`` - repository architecture, string. This field is read-only and generated automatically from run options if possible.
* ``name`` - repository name, string. This field is read-only and generated automatically from run options if possible.
* ``root`` - root path for application, string, required.
``sign:*`` groups

View File

@@ -1,36 +1,36 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --group ../pyproject.toml:docs --extra s3 --extra validator --extra web --output-file ../docs/requirements.txt ../pyproject.toml
# uv pip compile --group pyproject.toml:docs --extra s3 --extra validator --extra web --output-file docs/requirements.txt pyproject.toml
aiohappyeyeballs==2.6.1
# via aiohttp
aiohttp==3.11.18
# via
# ahriman (../pyproject.toml)
# ahriman (pyproject.toml)
# aiohttp-cors
# aiohttp-jinja2
aiohttp-cors==0.8.1
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
aiohttp-jinja2==1.6
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
aiosignal==1.3.2
# via aiohttp
alabaster==1.0.0
# via sphinx
argparse-manpage==4.6
# via ahriman (../pyproject.toml:docs)
# via ahriman (pyproject.toml:docs)
attrs==25.3.0
# via aiohttp
babel==2.17.0
# via sphinx
bcrypt==4.3.0
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
boto3==1.38.11
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
botocore==1.38.11
# via
# boto3
# s3transfer
cerberus==1.3.7
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
certifi==2025.4.26
# via requests
charset-normalizer==3.4.2
@@ -51,7 +51,7 @@ idna==3.10
imagesize==1.4.1
# via sphinx
inflection==0.5.1
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
jinja2==3.1.6
# via
# aiohttp-jinja2
@@ -73,37 +73,37 @@ propcache==0.3.1
# aiohttp
# yarl
pydeps==3.0.1
# via ahriman (../pyproject.toml:docs)
# via ahriman (pyproject.toml:docs)
pyelftools==0.32
# via ahriman (../pyproject.toml)
# via ahriman (pyproject.toml)
pygments==2.19.1
# via sphinx
python-dateutil==2.9.0.post0
# via botocore
requests==2.32.3
# via
# ahriman (../pyproject.toml)
# ahriman (pyproject.toml)
# sphinx
roman-numerals-py==3.1.0
# via sphinx
s3transfer==0.12.0
# via boto3
shtab==1.7.2
# via ahriman (../pyproject.toml:docs)
# via ahriman (pyproject.toml:docs)
six==1.17.0
# via python-dateutil
snowballstemmer==3.0.0.1
# via sphinx
sphinx==8.2.3
# via
# ahriman (../pyproject.toml:docs)
# ahriman (pyproject.toml:docs)
# sphinx-argparse
# sphinx-rtd-theme
# sphinxcontrib-jquery
sphinx-argparse==0.5.2
# via ahriman (../pyproject.toml:docs)
# via ahriman (pyproject.toml:docs)
sphinx-rtd-theme==3.0.2
# via ahriman (../pyproject.toml:docs)
# via ahriman (pyproject.toml:docs)
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0

View File

@@ -2,7 +2,7 @@
pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.18.2
pkgver=2.19.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')

View File

@@ -55,6 +55,11 @@
<i class="bi bi-play"></i> update
</button>
</li>
<li>
<button id="update-repositories-button" class="btn dropdown-item" onclick="refreshDatabases()">
<i class="bi bi-arrow-down-circle"></i> update pacman databases
</button>
</li>
<li>
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal">
<i class="bi bi-arrow-clockwise"></i> rebuild

View File

@@ -24,6 +24,13 @@
<datalist id="package-add-known-packages-dlist"></datalist>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label"></label>
<div class="col-9">
<input id="package-add-refresh-input" type="checkbox" class="form-check-input" value="" checked>
<label for="package-add-refresh-input" class="form-check-label">update pacman databases</label>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button id="package-add-variable-button" type="button" class="form-control btn btn-light rounded" onclick="packageAddVariableInputCreate()"><i class="bi bi-plus"></i> add environment variable </button>
@@ -50,6 +57,8 @@
const packageAddVariablesDiv = document.getElementById("package-add-variables-div");
const packageAddRefreshInput = document.getElementById("package-add-refresh-input");
function packageAddVariableInputCreate() {
const variableInput = document.createElement("div");
variableInput.classList.add("input-group");
@@ -99,16 +108,18 @@
return {patches: patches};
}
function packagesAdd(packages, patches, repository) {
function packagesAdd(packages, patches, repository, data) {
packages = packages ?? packageAddInput.value;
patches = patches ?? patchesParse();
repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
data = data ?? {refresh: packageAddRefreshInput.checked};
if (packages) {
bootstrap.Modal.getOrCreateInstance(packageAddModal).hide();
const onSuccess = update => `Packages ${update} have been added`;
const onFailure = error => `Package addition failed: ${error}`;
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches);
const parameters = Object.assign({}, data, patches);
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, parameters);
}
}

View File

@@ -95,6 +95,9 @@
</div>
<div class="modal-footer">
{% if not auth.enabled or auth.username is not none %}
<input id="package-info-refresh-input" type="checkbox" class="form-check-input" value="" checked>
<label for="package-info-refresh-input" class="form-check-label">update pacman databases</label>
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
{% endif %}
@@ -135,6 +138,8 @@
const packageInfoVariablesBlock = document.getElementById("package-info-variables-block");
const packageInfoVariablesDiv = document.getElementById("package-info-variables-div");
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
function clearChart() {
packageInfoEventsUpdateChartCanvas.hidden = true;
if (packageInfoEventsUpdateChart) {
@@ -404,7 +409,7 @@
function packageInfoUpdate() {
const packageBase = packageInfoModal.package;
packagesAdd(packageBase, [], repository);
packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked});
}
function showPackageInfo(packageBase) {

View File

@@ -73,6 +73,19 @@
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
}
function refreshDatabases() {
const onSuccess = _ => "Pacman database update has been requested";
const onFailure = error => `Could not update pacman databases: ${error}`;
const parameters = {
refresh: true,
aur: false,
local: false,
manual: false,
};
doPackageAction("/api/v1/service/update", [], repository, onSuccess, onFailure, parameters);
}
function reload() {
table.bootstrapTable("showLoading");

View File

@@ -674,6 +674,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,6 +1,6 @@
.TH AHRIMAN "1" "2025\-06\-16" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2026\-01\-25" "ahriman 2.19.2" "ArcH linux ReposItory MANager"
.SH NAME
ahriman
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} ...

View File

@@ -99,6 +99,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 +116,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 +127,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 +140,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 +152,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 +163,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 +182,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 +202,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 +252,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 +278,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 +306,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 +326,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 +360,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 +370,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 +380,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 +412,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 +449,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 +461,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 +472,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 +514,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 +533,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 +547,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 +590,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 +612,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 +663,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 +689,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 +701,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 +712,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 +764,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 +799,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 +828,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 +864,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 +876,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 +886,41 @@ _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() {
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

View File

@@ -58,23 +58,23 @@ web = [
"aiohttp_cors",
"aiohttp_jinja2",
]
web_api-docs = [
"ahriman[web]",
"aiohttp-apispec",
"setuptools", # required by aiohttp-apispec
]
web_auth = [
web-auth = [
"ahriman[web]",
"aiohttp_session",
"aiohttp_security",
"cryptography",
]
web_metrics = [
web-docs = [
"ahriman[web]",
"aiohttp-apispec",
"setuptools", # required by aiohttp-apispec
]
web-metrics = [
"ahriman[web]",
"aiohttp-openmetrics",
]
web_oauth2 = [
"ahriman[web_auth]",
web-oauth2 = [
"ahriman[web-auth]",
"aioauth-client",
]

View File

@@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock
@@ -62,7 +62,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_REPOSITORY_SERVER: http://frontend/repo/$$repo/$$arch

View File

@@ -12,7 +12,7 @@ services:
AHRIMAN_PACMAN_MIRROR: https://de.mirror.archlinux32.org/$$arch/$$repo
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@@ -8,8 +8,8 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_POSTSETUP_COMMAND: ahriman --architecture x86_64 --repository another-demo service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_PRESETUP_COMMAND: ahriman --architecture x86_64 --repository another-demo service-setup --build-as-user ahriman --packager 'ahriman bot <ahriman@example.com>'
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@@ -9,7 +9,7 @@ services:
AHRIMAN_OAUTH_CLIENT_SECRET: ${AHRIMAN_OAUTH_CLIENT_SECRET}
AHRIMAN_OUTPUT: console
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p ""
AHRIMAN_POSTSETUP_COMMAND: sudo -u ahriman ahriman user-add ${AHRIMAN_OAUTH_USER} -R full -p ""
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

View File

@@ -6,7 +6,7 @@ services:
environment:
AHRIMAN_DEBUG: yes
AHRIMAN_OUTPUT: console
AHRIMAN_PRESETUP_COMMAND: sudo -u ahriman gpg --import /run/secrets/key
AHRIMAN_POSTSETUP_COMMAND: sudo -u ahriman gpg --import /run/secrets/key
AHRIMAN_REPOSITORY: ahriman-demo
configs:

View File

@@ -8,7 +8,7 @@ services:
AHRIMAN_OUTPUT: console
AHRIMAN_PASSWORD: ${AHRIMAN_PASSWORD}
AHRIMAN_PORT: 8080
AHRIMAN_PRESETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_POSTSETUP_COMMAND: (cat /run/secrets/password; echo; cat /run/secrets/password) | sudo -u ahriman ahriman user-add demo -R full
AHRIMAN_REPOSITORY: ahriman-demo
AHRIMAN_UNIX_SOCKET: /var/lib/ahriman/ahriman/ahriman.sock

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.18.2"
__version__ = "2.19.2"

View File

@@ -133,18 +133,18 @@ class Application(ApplicationPackages, ApplicationRepository):
if not process_dependencies or not packages:
return packages
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
def missing_dependencies(sources: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources
satisfied_packages = known_packages | {
single
for package in source
for single in package.packages_full
for source in sources
for single in source.packages_full
}
return {
dependency: package.packager
for package in source
for dependency in package.depends_build
dependency: source.packager
for source in sources
for dependency in source.depends_build
if dependency not in satisfied_packages
}
@@ -156,7 +156,7 @@ class Application(ApplicationPackages, ApplicationRepository):
# there is local cache, load package from it
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
else:
leaf = Package.from_aur(package_name, packager)
leaf = Package.from_aur(package_name, packager, include_provides=True)
portion[leaf.base] = leaf
# register package in the database

View File

@@ -72,16 +72,17 @@ class Setup(Handler):
application = Application(repository_id, configuration, report=report)
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)
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)
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
@staticmethod
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
@@ -280,6 +281,5 @@ class Setup(Handler):
command = Setup.build_command(paths.root, repository_id)
command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
paths.chown(command) # we would like to keep owner inside ahriman's home
arguments = [_set_service_setup_parser]

View File

@@ -52,7 +52,7 @@ class Validate(Handler):
"""
from ahriman.core.configuration.validator import Validator
schema = Validate.schema(repository_id, configuration)
schema = Validate.schema(configuration)
validator = Validator(configuration=configuration, schema=schema)
if validator.validate(configuration.dump()):
@@ -83,12 +83,11 @@ class Validate(Handler):
return parser
@staticmethod
def schema(repository_id: RepositoryId, configuration: Configuration) -> ConfigurationSchema:
def schema(configuration: Configuration) -> ConfigurationSchema:
"""
get schema with triggers
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
Returns:
@@ -107,12 +106,12 @@ class Validate(Handler):
continue
# default settings if any
for schema_name, schema in trigger_class.configuration_schema(repository_id, None).items():
for schema_name, schema in trigger_class.configuration_schema(None).items():
erased = Validate.schema_erase_required(copy.deepcopy(schema))
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), erased)
# settings according to enabled triggers
for schema_name, schema in trigger_class.configuration_schema(repository_id, configuration).items():
for schema_name, schema in trigger_class.configuration_schema(configuration).items():
root[schema_name] = Validate.schema_merge(root.get(schema_name, {}), copy.deepcopy(schema))
return root

View File

@@ -130,8 +130,8 @@ 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)
shutil.copy(src, dst)
self.repository_paths.chown(dst)
with self.repository_paths.preserve_owner(dst.parent):
shutil.copy(src, dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
"""
@@ -255,3 +255,20 @@ class Pacman(LazyLogging):
result.update(trim_package(provides) for provides in package.provides)
return result
def provided_by(self, package_name: str) -> Generator[Package, None, None]:
"""
search through databases and emit packages which provides the ``package_name``
Args:
package_name(str): package name to search
Yields:
Package: list of packages which were returned by the query
"""
def is_package_provided(package: Package) -> bool:
provides = [trim_package(name) for name in package.provides]
return package_name in provides
for database in self.handle.get_syncdbs():
yield from filter(is_package_provided, database.search(package_name))

View File

@@ -97,20 +97,17 @@ class AUR(Remote):
Returns:
list[AURPackage]: response parsed to package list
Raises:
PackageInfoError: if multiple arguments are passed
"""
query: list[tuple[str, str]] = [
("type", request_type),
("v", self.DEFAULT_RPC_VERSION),
]
if len(args) != 1:
raise PackageInfoError("AUR API requires exactly one argument to search")
arg_query = "arg[]" if len(args) > 1 else "arg"
for arg in args:
query.append((arg_query, arg))
url = f"{self.DEFAULT_RPC_URL}/v{self.DEFAULT_RPC_VERSION}/{request_type}/{args[0]}"
query = list(kwargs.items())
for key, value in kwargs.items():
query.append((key, value))
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
response = self.make_request("GET", url, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
@@ -133,15 +130,36 @@ class AUR(Remote):
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
def package_provided_by(self, package_name: str, *, pacman: Pacman | None) -> list[AURPackage]:
"""
get package list which provide the specified package name
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return [
package
# search api provides reduced models
for stub in self.package_search(package_name, pacman=pacman, search_by="provides")
# verity that found package actually provides it
if package_name in (package := self.package_info(stub.name, pacman=pacman)).provides
]
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return self.aur_request("search", *keywords, by="name-desc")
search_by = search_by or "name-desc"
return self.aur_request("search", *keywords, by=search_by)

View File

@@ -127,15 +127,17 @@ class Official(Remote):
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return self.arch_request(*keywords, by="q")
search_by = search_by or "q"
return self.arch_request(*keywords, by=search_by)

View File

@@ -59,3 +59,22 @@ class OfficialSyncdb(Official):
return next(AURPackage.from_pacman(package) for package in pacman.package(package_name))
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_provided_by(self, package_name: str, *, pacman: Pacman | None) -> list[AURPackage]:
"""
get package list which provide the specified package name
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria
"""
if pacman is None:
return []
return [
AURPackage.from_pacman(package)
for package in pacman.provided_by(package_name)
]

View File

@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.http import SyncHttpClient
from ahriman.models.aur_package import AURPackage
@@ -41,22 +42,36 @@ class Remote(SyncHttpClient):
"""
@classmethod
def info(cls, package_name: str, *, pacman: Pacman | None = None) -> AURPackage:
def info(cls, package_name: str, *, pacman: Pacman | None = None, include_provides: bool = False) -> AURPackage:
"""
get package info by its name
get package info by its name. If ``include_provides`` is set to ``True``, then, in addition, this method
will perform search by :attr:`ahriman.models.aur_package.AURPackage.provides` and return first package found.
Note, however, that in this case some implementation might not provide this method and search result will might
not be stable
Args:
package_name(str): package name to search
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns:
AURPackage: package which match the package name
Raises:
UnknownPackageError: if requested package not found
"""
return cls().package_info(package_name, pacman=pacman)
instance = cls()
try:
return instance.package_info(package_name, pacman=pacman)
except UnknownPackageError:
if include_provides and (provided_by := instance.package_provided_by(package_name, pacman=pacman)):
return next(iter(provided_by))
raise
@classmethod
def multisearch(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
def multisearch(cls, *keywords: str, pacman: Pacman | None = None,
search_by: str | None = None) -> list[AURPackage]:
"""
search in remote repository by using API with multiple words. This method is required in order to handle
https://bugs.archlinux.org/task/49133. In addition, short words will be dropped
@@ -65,6 +80,7 @@ class Remote(SyncHttpClient):
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
search_by(str | None, optional): search by keywords (Default value = None)
Returns:
list[AURPackage]: list of packages each of them matches all search terms
@@ -72,7 +88,7 @@ class Remote(SyncHttpClient):
instance = cls()
packages: dict[str, AURPackage] = {}
for term in filter(lambda word: len(word) >= 3, keywords):
portion = instance.search(term, pacman=pacman)
portion = instance.package_search(term, pacman=pacman, search_by=search_by)
packages = {
package.name: package # not mistake to group them by name
for package in portion
@@ -114,7 +130,7 @@ class Remote(SyncHttpClient):
raise NotImplementedError
@classmethod
def search(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
def search(cls, *keywords: str, pacman: Pacman | None = None, search_by: str | None = None) -> list[AURPackage]:
"""
search package in AUR web
@@ -122,11 +138,12 @@ class Remote(SyncHttpClient):
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
search_by(str | None, optional): search by keywords (Default value = None)
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return cls().package_search(*keywords, pacman=pacman)
return cls().package_search(*keywords, pacman=pacman, search_by=search_by)
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
@@ -144,13 +161,28 @@ class Remote(SyncHttpClient):
"""
raise NotImplementedError
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
def package_provided_by(self, package_name: str, *, pacman: Pacman | None) -> list[AURPackage]:
"""
get package list which provide the specified package name
Args:
package_name(str): package name to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria
"""
del package_name, pacman
return []
def package_search(self, *keywords: str, pacman: Pacman | None, search_by: str | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
search_by(str | None): search by keywords
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@@ -41,7 +41,6 @@ class Configuration(configparser.RawConfigParser):
SYSTEM_CONFIGURATION_PATH(Path): (class attribute) default system configuration path distributed by package
includes(list[Path]): list of includes which were read
path(Path | None): path to root configuration file
repository_id(RepositoryId | None): repository unique identifier
Examples:
Configuration class provides additional method in order to handle application configuration. Since this class is
@@ -91,7 +90,7 @@ class Configuration(configparser.RawConfigParser):
},
)
self.repository_id: RepositoryId | None = None
self._repository_id: RepositoryId | None = None
self.path: Path | None = None
self.includes: list[Path] = []
@@ -126,6 +125,32 @@ class Configuration(configparser.RawConfigParser):
"""
return self.getpath("settings", "logging")
@property
def repository_id(self) -> RepositoryId | None:
"""
repository identifier
Returns:
RepositoryId: repository unique identifier
"""
return self._repository_id
@repository_id.setter
def repository_id(self, repository_id: RepositoryId | None) -> None:
"""
setter for repository identifier
Args:
repository_id(RepositoryId | None): repository unique identifier
"""
self._repository_id = repository_id
if repository_id is None or repository_id.is_empty:
self.remove_option("repository", "name")
self.remove_option("repository", "architecture")
else:
self.set_option("repository", "name", repository_id.name)
self.set_option("repository", "architecture", repository_id.architecture)
@property
def repository_name(self) -> str:
"""
@@ -210,6 +235,17 @@ class Configuration(configparser.RawConfigParser):
raise InitializeError("Configuration path and/or repository id are not set")
return self.path, self.repository_id
def copy_from(self, configuration: Self) -> None:
"""
copy values from another instance overriding existing
Args:
configuration(Self): configuration instance to merge from
"""
for section in configuration.sections():
for key, value in configuration.items(section):
self.set_option(section, key, value)
def dump(self) -> dict[str, dict[str, str]]:
"""
dump configuration to dictionary
@@ -220,6 +256,7 @@ class Configuration(configparser.RawConfigParser):
return {
section: dict(self.items(section))
for section in self.sections()
if self[section]
}
# pylint and mypy are too stupid to find these methods

View File

@@ -57,7 +57,7 @@ class ConfigurationMultiDict(dict[str, Any]):
OptionError: if the key already exists in the dictionary, but not a single value list or a string
"""
match self.get(key):
case [current_value] | str(current_value):
case [current_value] | (str() as current_value):
value = f"{current_value} {value}"
case None:
pass

View File

@@ -254,6 +254,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"repository": {
"type": "dict",
"schema": {
"architecture": {
"type": "string",
"empty": False,
},
"name": {
"type": "string",
"empty": False,

View File

@@ -94,9 +94,13 @@ class SQLite(
sqlite3.register_adapter(list, json.dumps)
sqlite3.register_converter("json", json.loads)
if self._configuration.getboolean("settings", "apply_migrations", fallback=True):
if not self._configuration.getboolean("settings", "apply_migrations", fallback=True):
return
if self._repository_id.is_empty:
return # do not perform migration on empty repository identifier (e.g. multirepo command)
with self._repository_paths.preserve_owner():
self.with_connection(lambda connection: Migrations.migrate(connection, self._configuration))
self._repository_paths.chown(self.path)
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""

View File

@@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import requests
import sys
from functools import cached_property
from typing import Any, IO, Literal
@@ -70,7 +71,10 @@ class SyncHttpClient(LazyLogging):
request.Session: created session object
"""
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
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}"
return session

View File

@@ -33,6 +33,7 @@ class Leaf:
Attributes:
dependencies(set[str]): list of package dependencies
items(list[str]): list of packages in this leaf including provides
package(Package): leaf package properties
"""
@@ -42,17 +43,9 @@ class Leaf:
package(Package): package properties
"""
self.package = package
# store frequently used properties
self.dependencies = package.depends_build
@property
def items(self) -> Iterable[str]:
"""
extract all packages from the leaf
Returns:
Iterable[str]: packages containing in this leaf
"""
return self.package.packages.keys()
self.items = self.package.packages_full
def is_dependency(self, packages: Iterable[Leaf]) -> bool:
"""

View File

@@ -80,8 +80,7 @@ class Trigger(LazyLogging):
return self.repository_id.architecture
@classmethod
def configuration_schema(cls, repository_id: RepositoryId,
configuration: Configuration | None) -> ConfigurationSchema:
def configuration_schema(cls, configuration: Configuration | None) -> ConfigurationSchema:
"""
configuration schema based on supplied service configuration
@@ -89,7 +88,6 @@ class Trigger(LazyLogging):
Schema must be in cerberus format, for details and examples you can check built-in triggers.
Args:
repository_id(str): repository unique identifier
configuration(Configuration | None): configuration instance. If set to None, the default schema
should be returned
@@ -101,13 +99,15 @@ class Trigger(LazyLogging):
result: ConfigurationSchema = {}
for target in cls.configuration_sections(configuration):
if not configuration.has_section(target):
continue
section, schema_name = configuration.gettype(
target, repository_id, fallback=cls.CONFIGURATION_SCHEMA_FALLBACK)
if schema_name not in cls.CONFIGURATION_SCHEMA:
continue
result[section] = cls.CONFIGURATION_SCHEMA[schema_name]
for section in configuration.sections():
if not (section == target or section.startswith(f"{target}:")):
# either repository specific or exact name
continue
schema_name = configuration.get(section, "type", fallback=section)
if schema_name not in cls.CONFIGURATION_SCHEMA:
continue
result[section] = cls.CONFIGURATION_SCHEMA[schema_name]
return result

View File

@@ -136,7 +136,8 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
} | environment
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, env=full_environment, text=True, encoding="utf8", bufsize=1) as process:
user=user, env=full_environment, text=True, encoding="utf8", errors="backslashreplace",
bufsize=1) as process:
if input_data is not None:
input_channel = get_io(process, "stdin")
input_channel.write(input_data)

View File

@@ -25,7 +25,7 @@ from dataclasses import dataclass, field, fields
from pyalpm import Package # type: ignore[import-not-found]
from typing import Any, Self
from ahriman.core.utils import filter_json, full_version
from ahriman.core.utils import filter_json, full_version, trim_package
@dataclass(frozen=True, kw_only=True)
@@ -103,6 +103,17 @@ class AURPackage:
keywords: list[str] = field(default_factory=list)
groups: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
"""
update packages lists accordingly
"""
object.__setattr__(self, "depends", [trim_package(package) for package in self.depends])
object.__setattr__(self, "make_depends", [trim_package(package) for package in self.make_depends])
object.__setattr__(self, "opt_depends", [trim_package(package) for package in self.opt_depends])
object.__setattr__(self, "check_depends", [trim_package(package) for package in self.check_depends])
object.__setattr__(self, "conflicts", [trim_package(package) for package in self.conflicts])
object.__setattr__(self, "provides", [trim_package(package) for package in self.provides])
@classmethod
def from_json(cls, dump: dict[str, Any]) -> Self:
"""

View File

@@ -213,18 +213,19 @@ class Package(LazyLogging):
)
@classmethod
def from_aur(cls, name: str, packager: str | None = None) -> Self:
def from_aur(cls, name: str, packager: str | None = None, *, include_provides: bool = False) -> Self:
"""
construct package properties from AUR page
Args:
name(str): package name (either base or normal name)
packager(str | None, optional): packager to be used for this build (Default value = None)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns:
Self: package properties
"""
package = AUR.info(name)
package = AUR.info(name, include_provides=include_provides)
remote = RemoteSource(
source=PackageSource.AUR,
@@ -310,7 +311,8 @@ class Package(LazyLogging):
)
@classmethod
def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True) -> Self:
def from_official(cls, name: str, pacman: Pacman, packager: str | None = None, *, use_syncdb: bool = True,
include_provides: bool = False) -> Self:
"""
construct package properties from official repository page
@@ -319,11 +321,13 @@ class Package(LazyLogging):
pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
use_syncdb(bool, optional): use pacman databases instead of official repositories RPC (Default value = True)
include_provides(bool, optional): search by provides if no exact match found (Default value = False)
Returns:
Self: package properties
"""
package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name)
impl = OfficialSyncdb if use_syncdb else Official
package = impl.info(name, pacman=pacman, include_provides=include_provides)
remote = RemoteSource(
source=PackageSource.Repository,

View File

@@ -83,12 +83,13 @@ class PackageDescription:
def __post_init__(self) -> None:
"""
update dependencies list accordingly
update packages lists accordingly
"""
self.depends = [trim_package(package) for package in self.depends]
self.opt_depends = [trim_package(package) for package in self.opt_depends]
self.make_depends = [trim_package(package) for package in self.make_depends]
self.opt_depends = [trim_package(package) for package in self.opt_depends]
self.check_depends = [trim_package(package) for package in self.check_depends]
self.provides = [trim_package(package) for package in self.provides]
@property
def filepath(self) -> Path | None:

View File

@@ -17,6 +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/>.
#
import contextlib
import os
import shutil
@@ -221,22 +222,14 @@ class RepositoryPaths(LazyLogging):
stat = path.stat()
return stat.st_uid, stat.st_gid
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
Args:
package_base(str): package base name
Returns:
Path: full path to directory for specified package base cache
"""
return self.cache / package_base
def chown(self, path: Path) -> None:
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
@@ -256,6 +249,56 @@ class RepositoryPaths(LazyLogging):
set_owner(path)
path = path.parent
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
Args:
package_base(str): package base name
Returns:
Path: full path to directory for specified package base cache
"""
return self.cache / package_base
@contextlib.contextmanager
def preserve_owner(self, path: Path | None = None) -> Generator[None, None, None]:
"""
perform any action preserving owner for any newly created file or directory
Args:
path(Path | None, optional): use this path as root instead of repository root (Default value = None)
Examples:
This method is designed to use as context manager when you are going to perform operations which might
change filesystem, especially if you are doing it under unsafe flag, e.g.::
>>> with paths.preserve_owner():
>>> paths.tree_create()
Note, however, that this method doesn't handle any exceptions and will eventually interrupt
if there will be any.
"""
path = path or self.root
def walk(root: Path) -> Generator[Path, None, None]:
# 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)
def tree_clear(self, package_base: str) -> None:
"""
clear package specific files
@@ -274,12 +317,13 @@ class RepositoryPaths(LazyLogging):
"""
if self.repository_id.is_empty:
return # do not even try to create tree in case if no repository id set
for directory in (
self.cache,
self.chroot,
self.packages,
self.pacman,
self.repository,
):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
self.chown(directory)
with self.preserve_owner():
for directory in (
self.cache,
self.chroot,
self.packages,
self.pacman,
self.repository,
):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)

View File

@@ -72,8 +72,8 @@ def setup_routes(application: Application, configuration: Configuration) -> None
application(Application): web application instance
configuration(Configuration): configuration instance
"""
application.router.add_static("/static", configuration.getpath("web", "static_path"), name="_static",
follow_symlinks=True)
application.router.add_static("/static", configuration.getpath("web", "static_path"),
name="_static", follow_symlinks=True)
for route, view in _dynamic_routes(configuration):
application.router.add_view(route, view, name=_identifier(route))

View File

@@ -166,11 +166,16 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
# package cache
if not repositories:
raise InitializeError("No repositories configured, exiting")
database = SQLite.load(configuration)
watchers: dict[RepositoryId, Watcher] = {}
configuration_path, _ = configuration.check_loaded()
for repository_id in repositories:
application.logger.info("load repository %s", repository_id)
client = Client.load(repository_id, configuration, database, report=False) # explicitly load local client
# load settings explicitly for architecture if any
repository_configuration = Configuration.from_path(configuration_path, repository_id)
# load database instance, because it holds identifier
database = SQLite.load(repository_configuration)
# explicitly load local client
client = Client.load(repository_id, repository_configuration, database, report=False)
watchers[repository_id] = Watcher(client)
application[WatcherKey] = watchers
# workers cache
@@ -179,6 +184,7 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
application[SpawnKey] = spawner
application.logger.info("setup authorization")
database = SQLite.load(configuration)
validator = application[AuthKey] = Auth.load(configuration, database)
if validator.enabled:
from ahriman.web.middlewares.auth_handler import setup_auth

View File

@@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2024 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@@ -1,4 +1,6 @@
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
from unittest.mock import MagicMock, call as MockCall
from ahriman.application.application import Application
@@ -73,6 +75,10 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
mock.packages_full = [package_base]
return mock
def get_package(name: str | Path, *args: Any, **kwargs: Any) -> Package:
name = name if isinstance(name, str) else name.name
return packages[name]
package_python_schedule.packages = {
package_python_schedule.base: package_python_schedule.packages[package_python_schedule.base]
}
@@ -87,10 +93,8 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
}
mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p.name == "python")
package_aur_mock = mocker.patch("ahriman.models.package.Package.from_aur",
side_effect=lambda *args: packages[args[0]])
package_local_mock = mocker.patch("ahriman.models.package.Package.from_build",
side_effect=lambda *args: packages[args[0].name])
package_aur_mock = mocker.patch("ahriman.models.package.Package.from_aur", side_effect=get_package)
package_local_mock = mocker.patch("ahriman.models.package.Package.from_build", side_effect=get_package)
packages_mock = mocker.patch("ahriman.application.application.Application._known_packages",
return_value={"devtools", "python-build", "python-pytest"})
status_client_mock = mocker.patch("ahriman.core.status.Client.set_unknown")
@@ -98,8 +102,8 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
result = application.with_dependencies([package_ahriman], process_dependencies=True)
assert {package.base: package for package in result} == packages
package_aur_mock.assert_has_calls([
MockCall(package_python_schedule.base, package_ahriman.packager),
MockCall("python-installer", package_ahriman.packager),
MockCall(package_python_schedule.base, package_ahriman.packager, include_provides=True),
MockCall("python-installer", package_ahriman.packager, include_provides=True),
], any_order=True)
package_local_mock.assert_has_calls([
MockCall(application.repository.paths.cache_for("python"), "x86_64", package_ahriman.packager),

View File

@@ -144,6 +144,7 @@ def test_repositories_extract(args: argparse.Namespace, configuration: Configura
args.architecture = "arch"
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")
@@ -159,6 +160,7 @@ def test_repositories_extract_repository(args: argparse.Namespace, configuration
"""
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"})
@@ -175,6 +177,7 @@ def test_repositories_extract_repository_legacy(args: argparse.Namespace, config
"""
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())
@@ -191,6 +194,7 @@ def test_repositories_extract_architecture(args: argparse.Namespace, configurati
"""
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")
@@ -207,6 +211,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())
@@ -221,6 +226,7 @@ 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")
@@ -236,6 +242,7 @@ 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")
@@ -251,6 +258,7 @@ 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())

View File

@@ -6,6 +6,7 @@ from pytest_mock import MockerFixture
from ahriman.application.application import Application
from ahriman.application.handlers.copy import Copy
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
@@ -30,11 +31,12 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
application_mock = mocker.patch("ahriman.application.handlers.copy.Copy.copy_package")
@@ -51,12 +53,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
package_ahriman: Package, mocker: MockerFixture) -> None:
database: SQLite, package_ahriman: Package, mocker: MockerFixture) -> None:
"""
must run command and remove packages afterward
"""
args = _default_args(args)
args.remove = True
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.application.handlers.copy.Copy.copy_package")
@@ -69,12 +72,14 @@ def test_run_remove(args: argparse.Namespace, configuration: Configuration, repo
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
database: SQLite, mocker: MockerFixture) -> None:
"""
must raise ExitCode exception on empty result
"""
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[])
mocker.patch("ahriman.application.application.Application.update")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")

View File

@@ -9,6 +9,7 @@ from urllib.parse import quote_plus as url_encode
from ahriman.application.handlers.setup import Setup
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import MissingArchitectureError
from ahriman.core.repository import Repository
from ahriman.models.repository_id import RepositoryId
@@ -44,11 +45,12 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
database: SQLite, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
ahriman_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_ahriman")
devtools_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_devtools")
@@ -56,9 +58,11 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo")
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.executable_create")
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
_, repository_id = configuration.check_loaded()
Setup.run(args, repository_id, configuration, report=False)
owner_guard_mock.assert_called_once_with()
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration)
devtools_configuration_mock.assert_called_once_with(
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}")
@@ -88,12 +92,13 @@ def test_run_no_architecture_or_repository(configuration: Configuration) -> None
def test_run_with_server(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
database: SQLite, mocker: MockerFixture) -> None:
"""
must run command with server specified
"""
args = _default_args(args)
args.server = "server"
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_ahriman")
mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_makepkg")
@@ -265,13 +270,11 @@ def test_executable_create(configuration: Configuration, repository_paths: Repos
"""
must create executable
"""
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
unlink_mock = mocker.patch("pathlib.Path.unlink")
_, repository_id = configuration.check_loaded()
Setup.executable_create(repository_paths, repository_id)
chown_mock.assert_called_once_with(Setup.build_command(repository_paths.root, repository_id))
symlink_mock.assert_called_once_with(Setup.ARCHBUILD_COMMAND_PATH)
unlink_mock.assert_called_once_with(missing_ok=True)

View File

@@ -51,7 +51,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, database: S
update_mock.assert_called_once_with(user)
def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration, database: SQLite,
mocker: MockerFixture) -> None:
"""
must process users with empty password salt
"""
@@ -59,6 +60,7 @@ def test_run_empty_salt(args: argparse.Namespace, configuration: Configuration,
args = _default_args(args)
user = User(username=args.username, password=args.password, access=args.role,
packager_id=args.packager, key=args.key)
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.models.user.User.hash_password", return_value=user)
create_user_mock = mocker.patch("ahriman.application.handlers.users.Users.user_create", return_value=user)
update_mock = mocker.patch("ahriman.core.database.SQLite.user_update")

View File

@@ -2,6 +2,7 @@ import argparse
import json
import pytest
from pathlib import Path
from pytest_mock import MockerFixture
from ahriman.application.handlers.validate import Validate
@@ -53,12 +54,50 @@ def test_run_skip(args: argparse.Namespace, configuration: Configuration, mocker
print_mock.assert_not_called()
def test_run_default(args: argparse.Namespace, configuration: Configuration) -> None:
"""
must run on default configuration without errors
"""
args.exit_code = True
_, repository_id = configuration.check_loaded()
default = Configuration.from_path(Configuration.SYSTEM_CONFIGURATION_PATH, repository_id)
# copy autogenerated values
for section, key in (("build", "build_command"), ("repository", "root")):
value = configuration.get(section, key)
default.set_option(section, key, value)
Validate.run(args, repository_id, default, report=False)
def test_run_repo_specific_triggers(args: argparse.Namespace, configuration: Configuration,
resource_path_root: Path) -> None:
"""
must correctly insert repo specific triggers
"""
args.exit_code = True
_, repository_id = configuration.check_loaded()
# remove unused sections
for section in ("customs3", "github:x86_64", "logs-rotation", "mirrorlist"):
configuration.remove_section(section)
configuration.set_option("report", "target", "test")
for section in ("test", "test:i686", "test:another-repo:x86_64"):
configuration.set_option(section, "type", "html")
configuration.set_option(section, "link_path", "http://link_path")
configuration.set_option(section, "path", "path")
configuration.set_option(section, "template", "template")
configuration.set_option(section, "templates", str(resource_path_root))
Validate.run(args, repository_id, configuration, report=False)
def test_schema(configuration: Configuration) -> None:
"""
must generate full schema correctly
"""
_, repository_id = configuration.check_loaded()
schema = Validate.schema(repository_id, configuration)
schema = Validate.schema(configuration)
# defaults
assert schema.pop("console")
@@ -91,9 +130,7 @@ def test_schema_invalid_trigger(configuration: Configuration) -> None:
"""
configuration.set_option("build", "triggers", "some.invalid.trigger.path.Trigger")
configuration.remove_option("build", "triggers_known")
_, repository_id = configuration.check_loaded()
assert Validate.schema(repository_id, configuration) == CONFIGURATION_SCHEMA
assert Validate.schema(configuration) == CONFIGURATION_SCHEMA
def test_schema_erase_required() -> None:

View File

@@ -1575,6 +1575,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
args.command = ""
args.handler = Handler
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
mocker.patch("argparse.ArgumentParser.parse_args", return_value=args)
assert ahriman.run() == 1

View File

@@ -249,19 +249,25 @@ def auth(configuration: Configuration) -> Auth:
@pytest.fixture
def configuration(repository_id: RepositoryId, resource_path_root: Path) -> Configuration:
def configuration(repository_id: RepositoryId, tmp_path: Path, resource_path_root: Path) -> Configuration:
"""
configuration fixture
Args:
repository_id(RepositoryId): repository identifier fixture
tmp_path(Path): temporary path used by the fixture as root
resource_path_root(Path): resource path root directory
Returns:
Configuration: configuration test instance
"""
path = resource_path_root / "core" / "ahriman.ini"
return Configuration.from_path(path, repository_id)
instance = Configuration.from_path(path, repository_id)
instance.set_option("repository", "root", str(tmp_path))
instance.set_option("settings", "database", str(tmp_path / "ahriman.db"))
return instance
@pytest.fixture
@@ -275,9 +281,7 @@ def database(configuration: Configuration) -> SQLite:
Returns:
SQLite: database test instance
"""
database = SQLite.load(configuration)
yield database
database.path.unlink()
return SQLite.load(configuration)
@pytest.fixture

View File

@@ -4,7 +4,7 @@ import requests
from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from unittest.mock import MagicMock, call as MockCall
from ahriman.core.alpm.remote import AUR
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
@@ -76,24 +76,18 @@ def test_aur_request(aur: AUR, aur_package_ahriman: AURPackage,
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock)
assert aur.aur_request("info", "ahriman") == [aur_package_ahriman]
request_mock.assert_called_once_with(
"GET", "https://aur.archlinux.org/rpc",
params=[("type", "info"), ("v", "5"), ("arg", "ahriman")])
request_mock.assert_called_once_with("GET", "https://aur.archlinux.org/rpc/v5/info/ahriman", params=[])
def test_aur_request_multi_arg(aur: AUR, aur_package_ahriman: AURPackage,
mocker: MockerFixture, resource_path_root: Path) -> None:
def test_aur_request_multi_arg(aur: AUR) -> None:
"""
must perform request to AUR with multiple args
must raise PackageInfoError if invalid amount of arguments supplied
"""
response_mock = MagicMock()
response_mock.json.return_value = json.loads(_get_response(resource_path_root))
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock)
with pytest.raises(PackageInfoError):
aur.aur_request("search", "ahriman", "is", "cool")
assert aur.aur_request("search", "ahriman", "is", "cool") == [aur_package_ahriman]
request_mock.assert_called_once_with(
"GET", "https://aur.archlinux.org/rpc",
params=[("type", "search"), ("v", "5"), ("arg[]", "ahriman"), ("arg[]", "is"), ("arg[]", "cool")])
with pytest.raises(PackageInfoError):
aur.aur_request("search")
def test_aur_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage,
@@ -106,9 +100,8 @@ def test_aur_request_with_kwargs(aur: AUR, aur_package_ahriman: AURPackage,
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.make_request", return_value=response_mock)
assert aur.aur_request("search", "ahriman", by="name") == [aur_package_ahriman]
request_mock.assert_called_once_with(
"GET", "https://aur.archlinux.org/rpc",
params=[("type", "search"), ("v", "5"), ("arg", "ahriman"), ("by", "name")])
request_mock.assert_called_once_with("GET", "https://aur.archlinux.org/rpc/v5/search/ahriman",
params=[("by", "name")])
def test_aur_request_failed(aur: AUR, mocker: MockerFixture) -> None:
@@ -139,17 +132,46 @@ def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerF
def test_package_info_not_found(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must raise UnknownPackage exception in case if no package was found
must raise UnknownPackageError in case if no package was found
"""
mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name):
assert aur.package_info(aur_package_ahriman.name, pacman=None)
def test_package_provided_by(aur: AUR, aur_package_ahriman: AURPackage, aur_package_akonadi: AURPackage,
mocker: MockerFixture) -> None:
"""
must search for packages which provide required one
"""
aur_package_ahriman.provides.append(aur_package_ahriman.name)
search_mock = mocker.patch("ahriman.core.alpm.remote.AUR.package_search", return_value=[
aur_package_ahriman, aur_package_akonadi
])
info_mock = mocker.patch("ahriman.core.alpm.remote.AUR.package_info", side_effect=[
aur_package_ahriman, aur_package_akonadi
])
assert aur.package_provided_by(aur_package_ahriman.name, pacman=None) == [aur_package_ahriman]
search_mock.assert_called_once_with(aur_package_ahriman.name, pacman=None, search_by="provides")
info_mock.assert_has_calls([
MockCall(aur_package_ahriman.name, pacman=None), MockCall(aur_package_akonadi.name, pacman=None)
])
def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for search
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[aur_package_ahriman])
assert aur.package_search(aur_package_ahriman.name, pacman=None) == [aur_package_ahriman]
assert aur.package_search(aur_package_ahriman.name, pacman=None, search_by=None) == [aur_package_ahriman]
request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc")
def test_package_search_provides(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for search with custom field
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request")
aur.package_search(aur_package_ahriman.name, pacman=None, search_by="provides")
request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="provides")

View File

@@ -106,7 +106,7 @@ def test_package_info(official: Official, aur_package_akonadi: AURPackage, mocke
def test_package_info_not_found(official: Official, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must raise UnknownPackage exception in case if no package was found
must raise UnknownPackageError in case if no package was found
"""
mocker.patch("ahriman.core.alpm.remote.Official.arch_request", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name):
@@ -119,5 +119,16 @@ def test_package_search(official: Official, aur_package_akonadi: AURPackage, moc
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request",
return_value=[aur_package_akonadi])
assert official.package_search(aur_package_akonadi.name, pacman=None) == [aur_package_akonadi]
assert official.package_search(aur_package_akonadi.name, pacman=None, search_by=None) == [
aur_package_akonadi,
]
request_mock.assert_called_once_with(aur_package_akonadi.name, by="q")
def test_package_search_name(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for search with custom field
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request")
official.package_search(aur_package_akonadi.name, pacman=None, search_by="name")
request_mock.assert_called_once_with(aur_package_akonadi.name, by="name")

View File

@@ -16,18 +16,14 @@ def test_package_info(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURP
mocker.patch("ahriman.models.aur_package.AURPackage.from_pacman", return_value=aur_package_akonadi)
get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[aur_package_akonadi])
package = official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman)
assert official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman) == aur_package_akonadi
get_mock.assert_called_once_with(aur_package_akonadi.name)
assert package == aur_package_akonadi
def test_package_info_no_pacman(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURPackage,
mocker: MockerFixture) -> None:
def test_package_info_no_pacman(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURPackage) -> None:
"""
must raise UnknownPackageError if no pacman set
"""
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[aur_package_akonadi])
with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name):
official_syncdb.package_info(aur_package_akonadi.name, pacman=None)
@@ -40,3 +36,22 @@ def test_package_info_not_found(official_syncdb: OfficialSyncdb, aur_package_ako
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name):
assert official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman)
def test_package_provided_by(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
"""
must search by provides in database
"""
mocker.patch("ahriman.models.aur_package.AURPackage.from_pacman", return_value=aur_package_akonadi)
get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.provided_by", return_value=[aur_package_akonadi])
assert official_syncdb.package_provided_by(aur_package_akonadi.name, pacman=pacman) == [aur_package_akonadi]
get_mock.assert_called_once_with(aur_package_akonadi.name)
def test_package_provided_by_no_pacman(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURPackage) -> None:
"""
must return empty list if no pacman set
"""
assert official_syncdb.package_provided_by(aur_package_akonadi.name, pacman=None) == []

View File

@@ -5,16 +5,53 @@ from unittest.mock import call as MockCall
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import Remote
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.aur_package import AURPackage
def test_info(pacman: Pacman, mocker: MockerFixture) -> None:
def test_info(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
"""
must call info method
"""
info_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_info")
Remote.info("ahriman", pacman=pacman)
info_mock.assert_called_once_with("ahriman", pacman=pacman)
info_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_info", return_value=aur_package_ahriman)
assert Remote.info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman
info_mock.assert_called_once_with(aur_package_ahriman.name, pacman=pacman)
def test_info_not_found(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
"""
must raise UnknownPackageError if no package found and search by provides is disabled
"""
mocker.patch("ahriman.core.alpm.remote.Remote.package_info",
side_effect=UnknownPackageError(aur_package_ahriman.name))
with pytest.raises(UnknownPackageError):
Remote.info(aur_package_ahriman.name, pacman=pacman)
def test_info_include_provides(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
"""
must perform search through provides list is set
"""
mocker.patch("ahriman.core.alpm.remote.Remote.package_info",
side_effect=UnknownPackageError(aur_package_ahriman.name))
provided_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_provided_by",
return_value=[aur_package_ahriman])
assert Remote.info(aur_package_ahriman.name, pacman=pacman, include_provides=True) == aur_package_ahriman
provided_mock.assert_called_once_with(aur_package_ahriman.name, pacman=pacman)
def test_info_include_provides_not_found(aur_package_ahriman: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
"""
must raise UnknownPackageError if no package found and search by provides returns empty list
"""
mocker.patch("ahriman.core.alpm.remote.Remote.package_info",
side_effect=UnknownPackageError(aur_package_ahriman.name))
mocker.patch("ahriman.core.alpm.remote.Remote.package_provided_by", return_value=[])
with pytest.raises(UnknownPackageError):
Remote.info("ahriman", pacman=pacman, include_provides=True)
def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
@@ -22,10 +59,13 @@ def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: Mo
must search in AUR with multiple words
"""
terms = ["ahriman", "is", "cool"]
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.search", return_value=[aur_package_ahriman])
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[aur_package_ahriman])
assert Remote.multisearch(*terms, pacman=pacman) == [aur_package_ahriman]
search_mock.assert_has_calls([MockCall("ahriman", pacman=pacman), MockCall("cool", pacman=pacman)])
assert Remote.multisearch(*terms, pacman=pacman, search_by="name") == [aur_package_ahriman]
search_mock.assert_has_calls([
MockCall("ahriman", pacman=pacman, search_by="name"),
MockCall("cool", pacman=pacman, search_by="name"),
])
def test_multisearch_empty(pacman: Pacman, mocker: MockerFixture) -> None:
@@ -33,7 +73,7 @@ def test_multisearch_empty(pacman: Pacman, mocker: MockerFixture) -> None:
must return empty list if no long terms supplied
"""
terms = ["it", "is"]
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.search")
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
assert Remote.multisearch(*terms, pacman=pacman) == []
search_mock.assert_not_called()
@@ -43,9 +83,9 @@ def test_multisearch_single(aur_package_ahriman: AURPackage, pacman: Pacman, moc
"""
must search in AUR with one word
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.search", return_value=[aur_package_ahriman])
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[aur_package_ahriman])
assert Remote.multisearch("ahriman", pacman=pacman) == [aur_package_ahriman]
search_mock.assert_called_once_with("ahriman", pacman=pacman)
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by=None)
def test_remote_git_url(remote: Remote) -> None:
@@ -69,8 +109,8 @@ def test_search(pacman: Pacman, mocker: MockerFixture) -> None:
must call search method
"""
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
Remote.search("ahriman", pacman=pacman)
search_mock.assert_called_once_with("ahriman", pacman=pacman)
Remote.search("ahriman", pacman=pacman, search_by="name")
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by="name")
def test_package_info(remote: Remote, pacman: Pacman) -> None:
@@ -81,9 +121,16 @@ def test_package_info(remote: Remote, pacman: Pacman) -> None:
remote.package_info("package", pacman=pacman)
def test_package_provided_by(remote: Remote, pacman: Pacman) -> None:
"""
must return empty list for provides method
"""
assert remote.package_provided_by("package", pacman=pacman) == []
def test_package_search(remote: Remote, pacman: Pacman) -> None:
"""
must raise NotImplemented for missing package search method
"""
with pytest.raises(NotImplementedError):
remote.package_search("package", pacman=pacman)
remote.package_search("package", pacman=pacman, search_by=None)

View File

@@ -62,12 +62,12 @@ def test_database_copy(pacman: Pacman, mocker: MockerFixture) -> None:
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
copy_mock = mocker.patch("shutil.copy")
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
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)
chown_mock.assert_called_once_with(dst_path)
owner_guard_mock.assert_called_once_with(dst_path.parent)
def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:
@@ -282,3 +282,11 @@ def test_packages_with_provides(pacman: Pacman) -> None:
"""
assert "sh" in pacman.packages()
assert "mysql" in pacman.packages() # mariadb
def test_package_provided_by(pacman: Pacman) -> None:
"""
must search through the provides lists
"""
assert list(pacman.provided_by("sh"))
assert list(pacman.provided_by("libacl.so")) # case with exact version

View File

@@ -20,6 +20,40 @@ def test_architecture(configuration: Configuration) -> None:
assert configuration.architecture == "x86_64"
def test_repository_id(configuration: Configuration, repository_id: RepositoryId) -> None:
"""
must return repository identifier
"""
assert configuration.repository_id == repository_id
assert configuration.get("repository", "name") == repository_id.name
assert configuration.get("repository", "architecture") == repository_id.architecture
def test_repository_id_erase(configuration: Configuration) -> None:
"""
must remove repository identifier properties if empty identifier supplied
"""
configuration.repository_id = None
assert configuration.get("repository", "name", fallback=None) is None
assert configuration.get("repository", "architecture", fallback=None) is None
configuration.repository_id = RepositoryId("", "")
assert configuration.get("repository", "name", fallback=None) is None
assert configuration.get("repository", "architecture", fallback=None) is None
def test_repository_id_update(configuration: Configuration, repository_id: RepositoryId) -> None:
"""
must update repository identifier and related configuration options
"""
repository_id = RepositoryId("i686", repository_id.name)
configuration.repository_id = repository_id
assert configuration.repository_id == repository_id
assert configuration.get("repository", "name") == repository_id.name
assert configuration.get("repository", "architecture") == repository_id.architecture
def test_repository_name(configuration: Configuration) -> None:
"""
must return valid repository name
@@ -102,6 +136,15 @@ def test_check_loaded_architecture(configuration: Configuration) -> None:
configuration.check_loaded()
def test_copy_from(configuration: Configuration) -> None:
"""
must copy values from another instance
"""
instance = Configuration()
instance.copy_from(configuration)
assert instance.dump() == configuration.dump()
def test_dump(configuration: Configuration) -> None:
"""
dump must not be empty

View File

@@ -62,8 +62,8 @@ def test_validate_is_ip_address(validator: Validator, mocker: MockerFixture) ->
validator._validate_is_ip_address([], "field", "localhost")
validator._validate_is_ip_address([], "field", "127.0.0.1")
validator._validate_is_ip_address([], "field", "::")
validator._validate_is_ip_address([], "field", "0.0.0.0")
validator._validate_is_ip_address([], "field", "::") # nosec
validator._validate_is_ip_address([], "field", "0.0.0.0") # nosec
validator._validate_is_ip_address([], "field", "random string")

View File

@@ -36,6 +36,17 @@ def test_init_skip_migration(database: SQLite, mocker: MockerFixture) -> None:
migrate_schema_mock.assert_not_called()
def test_init_skip_empty_repository(database: SQLite, mocker: MockerFixture) -> None:
"""
must skip migrations if repository identifier is not set
"""
database._repository_id = RepositoryId("", "")
migrate_schema_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migrate")
database.init()
migrate_schema_mock.assert_not_called()
def test_package_clear(database: SQLite, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must clear package data

View File

@@ -195,6 +195,32 @@ def test_tree_levels_sorted() -> None:
assert third == [leaf2.package, leaf4.package]
def test_tree_levels_provides() -> None:
"""
must build tree according to provides list
"""
leaf1 = Leaf(
Package(
base="package1",
version="1.0.0",
remote=RemoteSource(source=PackageSource.AUR),
packages={"package1": PackageDescription(depends=["package3"])},
)
)
leaf2 = Leaf(
Package(
base="package2",
version="1.0.0",
remote=RemoteSource(source=PackageSource.AUR),
packages={"package2": PackageDescription(provides=["package3"])},
)
)
first, second = Tree([leaf1, leaf2]).levels()
assert first == [leaf2.package]
assert second == [leaf1.package]
def test_tree_partitions() -> None:
"""
must divide tree into partitions

View File

@@ -150,6 +150,13 @@ def test_check_output_empty_line(mocker: MockerFixture) -> None:
logger_mock.assert_has_calls([MockCall(""), MockCall("hello")])
def test_check_output_encoding_error(resource_path_root: Path) -> None:
"""
must correctly process unicode encoding error in command output
"""
assert check_output("cat", str(resource_path_root / "models" / "package_pacman-static_pkgbuild"))
def test_check_user(repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must check user correctly

View File

@@ -19,10 +19,9 @@ def test_configuration_schema(configuration: Configuration) -> None:
"""
section = "console"
configuration.set_option("report", "target", section)
_, repository_id = configuration.check_loaded()
expected = {section: ReportTrigger.CONFIGURATION_SCHEMA[section]}
assert ReportTrigger.configuration_schema(repository_id, configuration) == expected
assert ReportTrigger.configuration_schema(configuration) == expected
def test_configuration_schema_no_section(configuration: Configuration) -> None:
@@ -31,9 +30,7 @@ def test_configuration_schema_no_section(configuration: Configuration) -> None:
"""
section = "abracadabra"
configuration.set_option("report", "target", section)
_, repository_id = configuration.check_loaded()
assert ReportTrigger.configuration_schema(repository_id, configuration) == {}
assert ReportTrigger.configuration_schema(configuration) == {}
def test_configuration_schema_no_schema(configuration: Configuration) -> None:
@@ -43,17 +40,15 @@ def test_configuration_schema_no_schema(configuration: Configuration) -> None:
section = "abracadabra"
configuration.set_option("report", "target", section)
configuration.set_option(section, "key", "value")
_, repository_id = configuration.check_loaded()
assert ReportTrigger.configuration_schema(repository_id, configuration) == {}
assert ReportTrigger.configuration_schema(configuration) == {}
def test_configuration_schema_empty(configuration: Configuration) -> None:
"""
must return default schema if no configuration set
"""
_, repository_id = configuration.check_loaded()
assert ReportTrigger.configuration_schema(repository_id, None) == ReportTrigger.CONFIGURATION_SCHEMA
assert ReportTrigger.configuration_schema(None) == ReportTrigger.CONFIGURATION_SCHEMA
def test_configuration_schema_variables() -> None:

View File

@@ -2,7 +2,7 @@ import datetime
import json
import pyalpm # typing: ignore
from dataclasses import asdict, fields
from dataclasses import asdict, fields, replace
from pathlib import Path
from pytest_mock import MockerFixture
from typing import Any
@@ -38,6 +38,25 @@ def _get_official_data(resource_path_root: Path) -> dict[str, Any]:
return json.loads(response)["results"][0]
def test_post_init(aur_package_ahriman: AURPackage) -> None:
"""
must trim versions and descriptions from packages list
"""
package = replace(
aur_package_ahriman,
depends=["a=1"],
make_depends=["b>=3"],
opt_depends=["c: a description"],
check_depends=["d=4"],
provides=["e=5"],
)
assert package.depends == ["a"]
assert package.make_depends == ["b"]
assert package.opt_depends == ["c"]
assert package.check_depends == ["d"]
assert package.provides == ["e"]
def test_from_json(aur_package_ahriman: AURPackage, resource_path_root: Path) -> None:
"""
must load package from json

View File

@@ -167,15 +167,26 @@ def test_from_aur(package_ahriman: Package, aur_package_ahriman: AURPackage, moc
"""
must construct package from aur
"""
mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman)
info_mock = mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman)
package = Package.from_aur(package_ahriman.base, package_ahriman.packager)
info_mock.assert_called_once_with(package_ahriman.base, include_provides=False)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()
assert package_ahriman.packager == package.packager
def test_from_aur_include_provides(package_ahriman: Package, aur_package_ahriman: AURPackage,
mocker: MockerFixture) -> None:
"""
must construct package from aur by using provides list
"""
info_mock = mocker.patch("ahriman.core.alpm.remote.AUR.info", return_value=aur_package_ahriman)
Package.from_aur(package_ahriman.base, package_ahriman.packager, include_provides=True)
info_mock.assert_called_once_with(package_ahriman.base, include_provides=True)
def test_from_build(package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
"""
must construct package from PKGBUILD
@@ -269,14 +280,25 @@ def test_from_json_view_3(package_tpacpi_bat_git: Package) -> None:
assert Package.from_json(package_tpacpi_bat_git.view()) == package_tpacpi_bat_git
def test_from_official_include_provides(package_ahriman: Package, aur_package_ahriman: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
"""
must construct package from official repository
"""
info_mock = mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman)
Package.from_official(package_ahriman.base, pacman, package_ahriman.packager, include_provides=True)
info_mock.assert_called_once_with(package_ahriman.base, pacman=pacman, include_provides=True)
def test_from_official(package_ahriman: Package, aur_package_ahriman: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
"""
must construct package from official repository
"""
mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman)
info_mock = mocker.patch("ahriman.core.alpm.remote.Official.info", return_value=aur_package_ahriman)
package = Package.from_official(package_ahriman.base, pacman, package_ahriman.packager)
info_mock.assert_called_once_with(package_ahriman.base, pacman=pacman, include_provides=False)
assert package_ahriman.base == package.base
assert package_ahriman.version == package.version
assert package_ahriman.packages.keys() == package.packages.keys()

View File

@@ -6,10 +6,15 @@ from ahriman.models.package_description import PackageDescription
def test_post_init() -> None:
"""
must trim versions and descriptions from dependencies list
must trim versions and descriptions from packages list
"""
assert PackageDescription(depends=["a=1"], make_depends=["b>=3"], opt_depends=["c: a description"]) == \
PackageDescription(depends=["a"], make_depends=["b"], opt_depends=["c"])
assert PackageDescription(
depends=["a=1"],
make_depends=["b>=3"],
opt_depends=["c: a description"],
check_depends=["d=4"],
provides=["e=5"]
) == PackageDescription(depends=["a"], make_depends=["b"], opt_depends=["c"], check_depends=["d"], provides=["e"])
def test_filepath(package_description_ahriman: PackageDescription) -> None:

View File

@@ -198,15 +198,6 @@ def test_owner(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None
assert RepositoryPaths.owner(repository_paths.root) == (42, 142)
def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
"""
must return correct path for cache directory
"""
path = repository_paths.cache_for(package_ahriman.base)
assert path.name == package_ahriman.base
assert path.parent == repository_paths.cache
def test_chown(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
"""
must correctly set owner for the directory
@@ -216,7 +207,7 @@ def test_chown(repository_paths: RepositoryPaths, mocker: MockerFixture) -> None
chown_mock = mocker.patch("os.chown")
path = repository_paths.root / "path"
repository_paths.chown(path)
repository_paths._chown(path)
chown_mock.assert_called_once_with(path, 42, 42, follow_symlinks=False)
@@ -229,7 +220,7 @@ def test_chown_parent(repository_paths: RepositoryPaths, mocker: MockerFixture)
chown_mock = mocker.patch("os.chown")
path = repository_paths.root / "parent" / "path"
repository_paths.chown(path)
repository_paths._chown(path)
chown_mock.assert_has_calls([
MockCall(path, 42, 42, follow_symlinks=False),
MockCall(path.parent, 42, 42, follow_symlinks=False)
@@ -245,7 +236,7 @@ def test_chown_skip(repository_paths: RepositoryPaths, mocker: MockerFixture) ->
chown_mock = mocker.patch("os.chown")
path = repository_paths.root / "path"
repository_paths.chown(path)
repository_paths._chown(path)
chown_mock.assert_not_called()
@@ -254,7 +245,46 @@ def test_chown_invalid_path(repository_paths: RepositoryPaths) -> None:
must raise invalid path exception in case if directory outside the root supplied
"""
with pytest.raises(PathError):
repository_paths.chown(repository_paths.root.parent)
repository_paths._chown(repository_paths.root.parent)
def test_cache_for(repository_paths: RepositoryPaths, package_ahriman: Package) -> None:
"""
must return correct path for cache directory
"""
path = repository_paths.cache_for(package_ahriman.base)
assert path.name == package_ahriman.base
assert path.parent == repository_paths.cache
def test_preserve_owner(tmp_path: Path, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must preserve file owner during operations
"""
repository_paths = RepositoryPaths(tmp_path, repository_id)
repository_paths.tree_create()
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths._chown")
with repository_paths.preserve_owner():
(repository_paths.root / "created1").touch()
(repository_paths.chroot / "created2").touch()
chown_mock.assert_has_calls([MockCall(repository_paths.root / "created1")])
def test_preserve_owner_specific(tmp_path: Path, repository_id: RepositoryId, mocker: MockerFixture) -> None:
"""
must preserve file owner during operations only in specific directory
"""
repository_paths = RepositoryPaths(tmp_path, repository_id)
repository_paths.tree_create()
(repository_paths.root / "content").mkdir()
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths._chown")
with repository_paths.preserve_owner(repository_paths.root / "content"):
(repository_paths.root / "created1").touch()
(repository_paths.root / "content" / "created2").touch()
(repository_paths.chroot / "created3").touch()
chown_mock.assert_has_calls([MockCall(repository_paths.root / "content" / "created2")])
def test_tree_clear(repository_paths: RepositoryPaths, package_ahriman: Package, mocker: MockerFixture) -> None:
@@ -293,11 +323,11 @@ def test_tree_create(repository_paths: RepositoryPaths, mocker: MockerFixture) -
and not callable(getattr(repository_paths, prop))
}
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
repository_paths.tree_create()
mkdir_mock.assert_has_calls([MockCall(mode=0o755, parents=True, exist_ok=True) for _ in paths], any_order=True)
chown_mock.assert_has_calls([MockCall(pytest.helpers.anyvar(int)) for _ in paths], any_order=True)
owner_guard_mock.assert_called_once_with()
def test_tree_create_skip(mocker: MockerFixture) -> None:

View File

@@ -1,7 +1,6 @@
[settings]
include = .
logging = logging.ini
database = ../../../ahriman-test.db
[alpm]
database = /var/lib/pacman
@@ -31,7 +30,6 @@ triggers_known = ahriman.core.distributed.WorkerLoaderTrigger ahriman.core.distr
[repository]
name = aur
root = ../../../
[sign]
target =

19
tools/__init__.py Normal file
View File

@@ -0,0 +1,19 @@
#
# Copyright (c) 2021-2025 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/>.
#

View File

@@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2023 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

View File

@@ -1,5 +1,5 @@
#
# Copyright (c) 2021-2023 ahriman team.
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).

119
tox.ini
View File

@@ -1,119 +0,0 @@
[tox]
envlist = check, tests
isolated_build = true
labels =
release = version, docs, publish
dependencies = -e .[journald,pacman,reports,s3,shell,stats,unixsocket,validator,web,web_api-docs,web_auth,web_oauth2,web_metrics]
project_name = ahriman
[mypy]
flags = --implicit-reexport --strict --allow-untyped-decorators --allow-subclassing-any
[pytest]
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
asyncio_default_fixture_loop_scope = function
asyncio_mode = auto
spec_test_format = {result} {docstring_summary}
[testenv:archive]
description = Create source files tarball
deps =
build
commands =
python -m build --sdist
[testenv:check]
description = Run common checks like linter, mypy, etc
dependency_groups =
check
deps =
{[tox]dependencies}
pip_pre = true
setenv =
CFLAGS="-Wno-unterminated-string-initialization"
MYPYPATH=src
commands =
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/{[tox]project_name}" "tests/{[tox]project_name}"
pylint --rcfile=.pylint.toml "src/{[tox]project_name}"
bandit -c .bandit.yml -r "src/{[tox]project_name}"
bandit -c .bandit-test.yml -r "tests/{[tox]project_name}"
mypy {[mypy]flags} -p "{[tox]project_name}" --install-types --non-interactive
[testenv:docs]
description = Generate source files for documentation
allowlist_externals =
bash
find
mv
changedir = src
dependency_groups =
docs
depends =
version
deps =
{[tox]dependencies}
uv
pip_pre = true
setenv =
SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance
commands =
bash -c 'shtab --shell bash --prefix ahriman --prog ahriman ahriman.application.ahriman._parser > ../package/share/bash-completion/completions/_ahriman'
bash -c 'shtab --shell zsh --prefix ahriman --prog ahriman ahriman.application.ahriman._parser > ../package/share/zsh/site-functions/_ahriman'
argparse-manpage --module ahriman.application.ahriman --function _parser --author "ahriman team" --project-name ahriman --author-email "" --url https://github.com/arcan1s/ahriman --output ../package/share/man/man1/ahriman.1
pydeps ahriman --no-output --show-dot --dot-output architecture.dot --no-config --cluster
mv architecture.dot ../docs/_static/architecture.dot
# remove autogenerated modules rst files
find ../docs -type f -name "{[tox]project_name}*.rst" -delete
sphinx-apidoc -o ../docs .
# compile list of dependencies for rtd.io
uv pip compile --group ../pyproject.toml:docs --extra s3 --extra validator --extra web --output-file ../docs/requirements.txt --quiet ../pyproject.toml
[testenv:html]
description = Generate html documentation
dependency_groups =
docs
deps =
{[tox]dependencies}
pip_pre = true
recreate = true
commands =
sphinx-build -b html -a -j auto -W docs {envtmpdir}{/}html
[testenv:publish]
description = Create and publish release to GitHub
allowlist_externals =
git
depends =
docs
passenv =
SSH_AUTH_SOCK
commands =
git add package/archlinux/PKGBUILD src/ahriman/__init__.py docs/_static/architecture.dot package/share/man/man1/ahriman.1 package/share/bash-completion/completions/_ahriman package/share/zsh/site-functions/_ahriman
git commit -m "Release {posargs}"
git tag "{posargs}"
git push
git push --tags
[testenv:tests]
description = Run tests
dependency_groups =
tests
deps =
{[tox]dependencies}
pip_pre = true
setenv =
CFLAGS="-Wno-unterminated-string-initialization"
commands =
pytest {posargs}
[testenv:version]
description = Bump package version
allowlist_externals =
sed
deps =
packaging
commands =
# check if version is set and validate it
{envpython} -c 'from packaging.version import Version; Version("{posargs}")'
sed -i 's/^__version__ = .*/__version__ = "{posargs}"/' src/ahriman/__init__.py
sed -i "s/pkgver=.*/pkgver={posargs}/" package/archlinux/PKGBUILD

310
tox.toml Normal file
View File

@@ -0,0 +1,310 @@
env_list = [
"check",
"tests",
]
isolated_build = true
labels.release = [
"version",
"docs",
"publish",
]
[flags]
autopep8 = [
"--max-line-length", "120",
"-aa",
]
bandit = [
"--configfile", ".bandit.yml",
]
manpage = [
"--author", "{[project]name} team",
"--author-email", "",
"--description", "ArcH linux ReposItory MANager",
"--manual-title", "ArcH linux ReposItory MANager",
"--project-name", "{[project]name}",
"--version", "{env:VERSION}",
"--url", "https://github.com/arcan1s/ahriman",
]
mypy = [
"--implicit-reexport",
"--strict",
"--allow-untyped-decorators",
"--allow-subclassing-any",
]
pydeps = [
"--no-config",
"--cluster",
]
pylint = [
"--rcfile", ".pylint.toml",
]
shtab = [
"--prefix", "{[project]name}",
"--prog", "{[project]name}",
"ahriman.application.ahriman._parser",
]
[project]
extras = [
"journald",
"pacman",
"reports",
"s3",
"shell",
"stats",
"unixsocket",
"validator",
"web",
"web-auth",
"web-docs",
"web-oauth2",
"web-metrics",
]
name = "ahriman"
[env.archive]
description = "Create source files tarball"
deps = [
"build",
]
commands = [
[
"{envpython}",
"-m", "build",
"--sdist",
],
]
[env.check]
description = "Run common checks like linter, mypy, etc"
dependency_groups = [
"check",
]
extras = [
{ replace = "ref", of = ["project", "extras"], extend = true },
]
pip_pre = true
set_env.CFLAGS = "-Wno-unterminated-string-initialization"
set_env.MYPYPATH = "src"
commands = [
[
"autopep8",
{ replace = "ref", of = ["flags", "autopep8"], extend = true },
"--exit-code",
"--in-place",
"--jobs", "0",
"--recursive",
"src/{[project]name}",
"tests/{[project]name}",
],
[
"pylint",
{ replace = "ref", of = ["flags", "pylint"], extend = true },
"src/{[project]name}",
],
[
"bandit",
{ replace = "ref", of = ["flags", "bandit"], extend = true },
"--recursive",
"src/{[project]name}",
],
[
"bandit",
{ replace = "ref", of = ["flags", "bandit"], extend = true },
"--skip", "B101,B105,B106",
"--recursive",
"src/{[project]name}",
],
[
"mypy",
{ replace = "ref", of = ["flags", "mypy"], extend = true },
"--install-types",
"--non-interactive",
"--package", "{[project]name}",
],
]
[env.docs]
description = "Generate source files for documentation"
dependency_groups = [
"docs",
]
depends = [
"version",
]
deps = [
"uv",
]
dynamic_version = "{[project]name}.__version__"
extras = [
{ replace = "ref", of = ["project", "extras"], extend = true },
]
# TODO: steamline shlex usage after https://github.com/iterative/shtab/pull/192 merge
handle_redirect = true
pip_pre = true
set_env.PYTHONPATH = "src"
set_env.SPHINX_APIDOC_OPTIONS = "members,no-undoc-members,show-inheritance"
commands = [
[
"shtab",
{ replace = "ref", of = ["flags", "shtab"], extend = true },
"--shell",
"bash",
">",
"package/share/bash-completion/completions/_ahriman",
],
[
"shtab",
{ replace = "ref", of = ["flags", "shtab"], extend = true },
"--shell",
"zsh",
">",
"package/share/zsh/site-functions/_ahriman",
],
[
"argparse-manpage",
{ replace = "ref", of = ["flags", "manpage"], extend = true },
"--module", "ahriman.application.ahriman",
"--function", "_parser",
"--output", "package/share/man/man1/ahriman.1",
],
[
"pydeps",
{ replace = "ref", of = ["flags", "pydeps"], extend = true },
"--dot-output", "{tox_root}/docs/_static/architecture.dot",
"--no-output",
"--show-dot",
"src/ahriman",
],
[
"sphinx-apidoc",
"--force",
"--no-toc",
"--output-dir", "docs",
"src",
],
# compile list of dependencies for rtd.io
[
"uv",
"pip",
"compile",
"--group", "pyproject.toml:docs",
"--extra", "s3",
"--extra", "validator",
"--extra", "web",
"--output-file", "docs/requirements.txt",
"--quiet",
"pyproject.toml",
],
]
[env.html]
description = "Generate html documentation"
dependency_groups = [
"docs",
]
extras = [
{ replace = "ref", of = ["project", "extras"], extend = true },
]
pip_pre = true
recreate = true
commands = [
[
"sphinx-build",
"--builder", "html",
"--fail-on-warning",
"--jobs", "auto",
"--write-all",
"docs",
"{envtmpdir}/html",
],
]
[env.publish]
description = "Create and publish release to GitHub"
allowlist_externals = [
"git",
]
depends = [
"docs",
]
pass_env = [
"SSH_AUTH_SOCK",
]
commands = [
[
"git",
"add",
"package/archlinux/PKGBUILD",
"src/ahriman/__init__.py",
"docs/_static/architecture.dot",
"package/share/man/man1/ahriman.1",
"package/share/bash-completion/completions/_ahriman",
"package/share/zsh/site-functions/_ahriman",
],
[
"git",
"commit",
"--message", "Release {posargs}",
],
[
"git",
"tag",
"{posargs}",
],
[
"git",
"push",
],
[
"git",
"push",
"--tags",
],
]
[env.tests]
description = "Run tests"
dependency_groups = [
"tests",
]
extras = [
{ replace = "ref", of = ["project", "extras"], extend = true },
]
pip_pre = true
set_env.CFLAGS = "-Wno-unterminated-string-initialization"
commands = [
[
"pytest",
{ replace = "posargs", extend = true },
],
]
[env.version]
description = "Bump package version"
allowlist_externals = [
"sed",
]
deps = [
"packaging",
]
commands = [
# check if version is set and validate it
[
"{envpython}",
"-c", "from packaging.version import Version; Version('{posargs}')",
],
[
"sed",
"--in-place",
"s/^__version__ = .*/__version__ = \"{posargs}\"/",
"src/ahriman/__init__.py",
],
[
"sed",
"--in-place",
"s/pkgver=.*/pkgver={posargs}/",
"package/archlinux/PKGBUILD",
],
]

128
toxfile.py Normal file
View File

@@ -0,0 +1,128 @@
#
# Copyright (c) 2021-2025 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/>.
#
import importlib
import shlex
import sys
from tox.config.sets import EnvConfigSet
from tox.config.types import Command
from tox.plugin import impl
from tox.session.state import State
from tox.tox_env.api import ToxEnv
def _extract_version(env_conf: EnvConfigSet, python_path: str | None = None) -> dict[str, str]:
"""
extract version dynamically and set VERSION environment variable
Args:
env_conf(EnvConfigSet): the core configuration object
python_path(str | None): python path variable if available
Returns:
dict[str, str]: environment variables which must be inserted
"""
import_path = env_conf["dynamic_version"]
if not import_path:
return {}
if python_path is not None:
sys.path.append(python_path)
module_name, variable_name = import_path.rsplit(".", maxsplit=1)
module = importlib.import_module(module_name)
version = getattr(module, variable_name)
# reset import paths
sys.path.pop()
return {"VERSION": version}
def _wrap_commands(env_conf: EnvConfigSet, shell: str = "bash") -> None:
"""
wrap commands into shell if there is redirect
Args:
env_conf(EnvConfigSet): the core configuration object
shell(str, optional): shell command to use (Default value = "bash")
"""
if not env_conf["handle_redirect"]:
return
# append shell just in case
env_conf["allowlist_externals"].append(shell)
for command in env_conf["commands"]:
if len(command.args) < 3: # command itself, redirect and output
continue
redirect, output = command.args[-2:]
if redirect not in (">", "2>", "&>"):
continue
command.args = [
shell,
"-c",
f"{Command(command.args[:-2]).shell} {redirect} {shlex.quote(output)}",
]
@impl
def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None:
"""
add a command line argument. This is the first hook to be called,
right after the logging setup and config source discovery.
Args:
env_conf(EnvConfigSet): the core configuration object
state(State): the global tox state object
"""
del state
env_conf.add_config(
keys=["dynamic_version"],
of_type=str,
default="",
desc="import path for the version variable",
)
env_conf.add_config(
keys=["handle_redirect"],
of_type=bool,
default=False,
desc="wrap commands to handle redirects if any",
)
@impl
def tox_before_run_commands(tox_env: ToxEnv) -> None:
"""
called before the commands set is executed
Args:
tox_env(ToxEnv): the tox environment being executed
"""
env_conf = tox_env.conf
set_env = env_conf["set_env"]
python_path = set_env.load("PYTHONPATH") if "PYTHONPATH" in set_env else None
set_env.update(_extract_version(env_conf, python_path))
_wrap_commands(env_conf)