Allow to use single web instance for any repository

This commit is contained in:
Evgenii Alekseev 2023-09-18 11:26:21 +03:00 committed by Evgenii Alekseev
parent 4eb187aead
commit 17e6573e7f
134 changed files with 1984 additions and 898 deletions

View File

@ -42,7 +42,7 @@ ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@exa
# validate configuration
ahriman service-config-validate --exit-code
# enable services
systemctl enable ahriman-web@x86_64-github
systemctl enable ahriman-web
systemctl enable ahriman@x86_64-github.timer
if [[ -z $MINIMAL_INSTALL ]]; then
# run web service (detached)

View File

@ -100,6 +100,14 @@ ahriman.application.handlers.remove\_unknown module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.repositories module
------------------------------------------------
.. automodule:: ahriman.application.handlers.repositories
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.restore module
-------------------------------------------

View File

@ -60,6 +60,14 @@ ahriman.core.formatters.printer module
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.repository\_printer module
--------------------------------------------------
.. automodule:: ahriman.core.formatters.repository_printer
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.formatters.status\_printer module
----------------------------------------------

View File

@ -140,6 +140,14 @@ ahriman.models.pkgbuild\_patch module
:no-undoc-members:
:show-inheritance:
ahriman.models.process\_status module
-------------------------------------
.. automodule:: ahriman.models.process_status
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.property module
------------------------------

View File

@ -172,6 +172,14 @@ ahriman.web.schemas.remote\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.repository\_id\_schema module
-------------------------------------------------
.. automodule:: ahriman.web.schemas.repository_id_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.search\_schema module
-----------------------------------------

View File

@ -28,6 +28,14 @@ ahriman.web.views.v1.status.packages module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.repositories module
-----------------------------------------------
.. automodule:: ahriman.web.views.v1.status.repositories
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.status.status module
-----------------------------------------

View File

@ -41,7 +41,7 @@ Base configuration settings.
* ``apply_migrations`` - perform migrations on application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations automatically.
* ``database`` - path to SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. Note, however, that the application will also load configuration files from the repository root, which is used, in particular, by setup subcommand.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
* ``suppress_http_log_errors`` - suppress http log errors, boolean, optional, default ``no``. If set to ``yes``, any http log errors (e.g. if web server is not available, but http logging is enabled) will be suppressed.
@ -103,10 +103,10 @@ Settings for signing packages or repository. Group name can refer to architectur
* ``target`` - configuration flag to enable signing, space separated list of strings, required. Allowed values are ``package`` (sign each package separately), ``repository`` (sign repository database file).
* ``key`` - default PGP key, string, required. This key will also be used for database signing if enabled.
``web:*`` groups
----------------
``web`` group
-------------
Web server settings. If any of ``host``/``port`` is not set, web integration will be disabled. Group name can refer to architecture, e.g. ``web:x86_64`` can be used for x86_64 architecture specific settings. This feature requires ``aiohttp`` libraries to be installed.
Web server settings. If any of ``host``/``port`` is not set, web integration will be disabled. This feature requires ``aiohttp`` libraries to be installed.
* ``address`` - optional address in form ``proto://host:port`` (``port`` can be omitted in case of default ``proto`` ports), will be used instead of ``http://{host}:{port}`` in case if set, string, optional. This option is required in case if ``OAuth`` provider is used.
* ``debug`` - enable debug toolbar, boolean, optional, default ``no``.

View File

@ -1073,7 +1073,7 @@ How to setup web service
port = 8080
#.
Start the web service ``systemctl enable --now ahriman-web@x86_64-aur-clone``.
Start the web service ``systemctl enable --now ahriman-web``.
How to enable basic authorization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -1130,7 +1130,7 @@ How to enable basic authorization
sudo -u ahriman ahriman user-add -r full my-first-user
#.
Restart web service ``systemctl restart ahriman-web@x86_64-aur-clone``.
Restart web service ``systemctl restart ahriman-web``.
How to enable OAuth authorization
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -1176,7 +1176,7 @@ How to enable OAuth authorization
When it will ask for the password leave it blank.
#.
Restart web service ``systemctl restart ahriman-web@x86_64-aur-clone``.
Restart web service ``systemctl restart ahriman-web``.
How to implement own interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -63,4 +63,4 @@ In order to migrate to new filesystem tree the following actions are required:
.. code-block:: shell
sudo systemctl enable --now ahriman@x86_64-aur-clone.timer
sudo systemctl enable --now ahriman-web@x86_64-aur-clone
sudo systemctl enable --now ahriman-web

View File

@ -81,7 +81,7 @@ Initial setup
.. code-block:: shell
systemctl enable --now ahriman-web@x86_64-aur-clone
systemctl enable --now ahriman-web
#.
Add packages by using ``ahriman package-add {package}`` command:

View File

@ -46,7 +46,7 @@ package() {
python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl"
# thanks too PEP517, which we all wanted, you need to install data files manually nowadays
pushd package && find . -type f -exec install -Dm644 "{}" "$pkgdir/usr/{}" \; && popd
pushd package && find . \( -type f -or -type l \) -exec install -Dm644 "{}" "$pkgdir/usr/{}" \; && popd
# keep usr/share configs as reference and copy them to /etc
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"

View File

@ -0,0 +1,12 @@
[Unit]
Description=ArcH linux ReposItory MANager web server
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman web
User=ahriman
Group=ahriman
[Install]
WantedBy=multi-user.target

View File

@ -1,12 +0,0 @@
[Unit]
Description=ArcH linux ReposItory MANager web server (%i)
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman --repository-id "%I" web
User=ahriman
Group=ahriman
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1 @@
ahriman-web.service

View File

@ -1,7 +1,7 @@
<!doctype html>
<html lang="en">
<head>
<title>{{ repository }}</title>
<title>ahriman</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
@ -16,7 +16,22 @@
{% include "utils/bootstrap-scripts.jinja2" %}
<div class="container">
<h1 id="badge-repository">ahriman</h1>
<nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="#"><img src="/static/logo.svg" width="30" height="30" alt=""> ahriman</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar-supported-content" aria-controls="navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbar-supported-content" class="collapse navbar-collapse">
<ul id="repositories" class="nav nav-tabs">
{% for repository in repositories %}
<li class="nav-item">
<a id="{{ repository.id }}-lnk" class="nav-link" href="#{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</a>
</li>
{% endfor %}
</ul>
</div>
</nav>
</div>
<div id="alert-placeholder" class="toast-container p3 top-0 start-50 translate-middle-x"></div>
@ -31,28 +46,33 @@
</button>
<ul class="dropdown-menu">
<li>
<button id="package-add-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
<button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
<i class="bi bi-plus"></i> add
</button>
</li>
<li>
<button id="package-update-btn" class="btn dropdown-item" onclick="updatePackages()" hidden>
<button id="package-update-button" class="btn dropdown-item" onclick="updatePackages()" hidden>
<i class="bi bi-play"></i> update
</button>
</li>
<li>
<button id="package-rebuild-btn" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<button id="package-update-all-button" class="btn dropdown-item" onclick="updateAllPackages()" hidden>
<i class="bi bi-play"></i> update all
</button>
</li>
<li>
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<i class="bi bi-arrow-clockwise"></i> rebuild
</button>
</li>
<li>
<button id="package-remove-btn" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
<button id="package-remove-button" class="btn dropdown-item" onclick="removePackages()" disabled hidden>
<i class="bi bi-trash"></i> remove
</button>
</li>
</ul>
<button id="key-import-btn" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
<button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
<i class="bi bi-key"></i> import key
</button>
{% endif %}
@ -117,7 +137,7 @@
{% if auth.enabled %}
<ul class="nav">
{% if auth.username is none %}
<li>{{ auth.control|safe }}</li>
<li>{{ auth.control | safe }}</li>
{% else %}
<li>
<form action="/api/v1/logout" method="post">

View File

@ -8,21 +8,21 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="key-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
<label for="key-import-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label>
<div class="col-sm-10">
<input id="key-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
<input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required>
</div>
</div>
<div class="form-group row">
<label for="key-server-input" class="col-sm-2 col-form-label">key server</label>
<label for="key-import-server-input" class="col-sm-2 col-form-label">key server</label>
<div class="col-sm-10">
<input id="key-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
<input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required>
</div>
</div>
<div class="form-group row">
<div class="col-sm-2"></div>
<div class="col-sm-10">
<pre class="language-less"><samp id="key-body-input" class="pre-scrollable language-less"></samp><button id="key-copy-btn" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
<pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
</div>
</div>
@ -39,24 +39,24 @@
const keyImportModal = $("#key-import-modal");
const keyImportForm = $("#key-import-form");
keyImportModal.on("hidden.bs.modal", () => {
keyBodyInput.text("");
keyImportBodyInput.text("");
keyImportForm.trigger("reset");
});
const keyBodyInput = $("#key-body-input");
const keyCopyButton = $("#key-copy-btn");
const keyImportBodyInput = $("#key-import-body-input");
const keyImportCopyButton = $("#key-import-copy-button");
const keyFingerprintInput = $("#key-fingerprint-input");
const keyServerInput = $("#key-server-input");
const keyImportFingerprintInput = $("#key-import-fingerprint-input");
const keyImportServerInput = $("#key-import-server-input");
async function copyPgpKey() {
const logs = keyBodyInput.text();
await copyToClipboard(logs, keyCopyButton);
const logs = keyImportBodyInput.text();
await copyToClipboard(logs, keyImportCopyButton);
}
function fetchPgpKey() {
const key = keyFingerprintInput.val();
const server = keyServerInput.val();
const key = keyImportFingerprintInput.val();
const server = keyImportServerInput.val();
if (key && server) {
$.ajax({
@ -64,14 +64,14 @@
data: {"key": key, "server": server},
type: "GET",
dataType: "json",
success: response => { keyBodyInput.text(response.key); },
success: response => { keyImportBodyInput.text(response.key); },
});
}
}
function importPgpKey() {
const key = keyFingerprintInput.val();
const server = keyServerInput.val();
const key = keyImportFingerprintInput.val();
const server = keyImportServerInput.val();
if (key && server) {
$.ajax({

View File

@ -8,18 +8,18 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">username</label>
<div class="col-sm-10">
<input id="username" type="text" class="form-control" placeholder="enter username" name="username" required>
<label for="login-username" class="col-sm-4 col-form-label">username</label>
<div class="col-sm-8">
<input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">password</label>
<div class="col-sm-10">
<label for="login-password" class="col-sm-4 col-form-label">password</label>
<div class="col-sm-8">
<div class="input-group">
<input id="password" type="password" class="form-control" placeholder="enter password" name="password" required>
<input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="show-hide-password-btn" class="bi bi-eye"></i></button>
<button class="btn btn-outline-secondary" type="button" onclick="showPassword()"><i id="login-show-hide-password-button" class="bi bi-eye"></i></button>
</div>
</div>
</div>
@ -34,8 +34,8 @@
</div>
<script>
const passwordInput = $("#password");
const showHidePasswordButton = $("#show-hide-password-btn");
const passwordInput = $("#login-password");
const showHidePasswordButton = $("#login-show-hide-password-button");
function showPassword() {
if (passwordInput.attr("type") === "password") {

View File

@ -8,10 +8,20 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="package-input" class="col-sm-2 col-form-label">package</label>
<div class="col-sm-10">
<input id="package-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="known-packages-dlist"></datalist>
<label for="package-add-repository-input" class="col-sm-4 col-form-label">repository</label>
<div class="col-sm-8">
<select id="package-add-repository-input" class="form-control" name="repository" required>
{% for repository in repositories %}
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="package-add-input" class="col-sm-4 col-form-label">package</label>
<div class="col-sm-8">
<input id="package-add-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required>
<datalist id="package-add-known-packages-dlist"></datalist>
</div>
</div>
</div>
@ -27,14 +37,19 @@
<script>
const packageAddModal = $("#package-add-modal");
const packageAddForm = $("#package-add-form");
packageAddModal.on("shown.bs.modal", () => {
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); });
const packageInput = $("#package-input");
const knownPackagesList = $("#known-packages-dlist");
packageInput.keyup(() => {
clearTimeout(packageInput.data("timeout"));
packageInput.data("timeout", setTimeout($.proxy(() => {
const value = packageInput.val();
const packageAddInput = $("#package-add-input");
const packageAddRepositoryInput = $("#package-add-repository-input");
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist");
packageAddInput.keyup(() => {
clearTimeout(packageAddInput.data("timeout"));
packageAddInput.data("timeout", setTimeout($.proxy(() => {
const value = packageAddInput.val();
if (value.length >= 3) {
$.ajax({
@ -49,7 +64,7 @@
option.innerText = `${pkg.package} (${pkg.description})`;
return option;
});
knownPackagesList.empty().append(options);
packageAddKnownPackagesList.empty().append(options);
},
});
}
@ -57,22 +72,24 @@
});
function packagesAdd() {
const packages = packageInput.val();
const packages = packageAddInput.val();
const repository = getRepositorySelector(packageAddRepositoryInput);
if (packages) {
packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been added`;
const onFailure = error => `Package addition failed: ${error}`;
doPackageAction("/api/v1/service/add", [packages], onSuccess, onFailure);
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure);
}
}
function packagesRequest() {
const packages = packageInput.val();
const packages = packageAddInput.val();
const repository = getRepositorySelector(packageAddRepositoryInput);
if (packages) {
packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been requested`;
const onFailure = error => `Package request failed: ${error}`;
doPackageAction("/api/v1/service/request", [packages], onSuccess, onFailure);
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure);
}
}
</script>

View File

@ -6,7 +6,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
</div>
<div class="modal-body">
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="logs-copy-btn" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
<pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button>
@ -22,7 +22,7 @@
const packageInfo = $("#package-info");
const packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = $("#logs-copy-btn");
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
async function copyLogs() {
const logs = packageInfoLogsInput.text();
@ -46,6 +46,10 @@
$.ajax({
url: `/api/v2/packages/${packageBase}/logs`,
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {

View File

@ -8,9 +8,19 @@
</div>
<div class="modal-body">
<div class="form-group row">
<label for="dependency-input" class="col-sm-4 col-form-label">dependency</label>
<label for="package-rebuild-repository-input" class="col-sm-4 col-form-label">repository</label>
<div class="col-sm-8">
<input id="dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
<select id="package-rebuild-repository-input" class="form-control" name="repository" required>
{% for repository in repositories %}
<option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-group row">
<label for="package-rebuild-dependency-input" class="col-sm-4 col-form-label">dependency</label>
<div class="col-sm-8">
<input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required>
</div>
</div>
</div>
@ -25,17 +35,23 @@
<script>
const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = $("#package-rebuild-form");
packageRebuildModal.on("shown.bs.modal", () => {
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
});
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
const dependencyInput = $("#dependency-input");
const packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
function packagesRebuild() {
const packages = dependencyInput.val();
const packages = packageRebuildDependencyInput.val();
const repository = getRepositorySelector(packageRebuildRepositoryInput);
if (packages) {
packageRebuildModal.modal("hide");
const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`;
const onFailure = error => `Repository rebuild failed: ${error}`;
doPackageAction("/api/v1/service/rebuild", [packages], onSuccess, onFailure);
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
}
}
</script>

View File

@ -1,9 +1,22 @@
<script>
const keyImportButton = $("#key-import-btn");
const packageAddButton = $("#package-add-btn");
const packageRebuildButton = $("#package-rebuild-btn");
const packageRemoveButton = $("#package-remove-btn");
const packageUpdateButton = $("#package-update-btn");
const keyImportButton = $("#key-import-button");
const packageAddButton = $("#package-add-button");
const packageRebuildButton = $("#package-rebuild-button");
const packageRemoveButton = $("#package-remove-button");
const packageUpdateButton = $("#package-update-button");
const packageUpdateAllButton = $("#package-update-all-button");
let repository = null;
$("#repositories a").on("click", (event) => {
const element = event.target;
repository = {
architecture: element.dataset.architecture,
repository: element.dataset.repository,
};
packageUpdateButton.html(`<i class="bi bi-play"></i> update ${safe(repository.repository)} (${safe(repository.architecture)})`);
$(`#${element.id}`).tab("show");
reload();
});
const table = $("#packages");
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
@ -35,13 +48,17 @@
});
});
const repositoryBadge = $("#badge-repository");
const statusBadge = $("#badge-status");
const versionBadge = $("#badge-version");
function doPackageAction(uri, packages, successText, failureText) {
function doPackageAction(uri, packages, repository, successText, failureText) {
const queryParams = $.param({
architecture: repository.architecture,
repository: repository.repository,
}); // it will never be empty btw
$.ajax({
url: uri,
url: `${uri}?${queryParams}`,
data: JSON.stringify({packages: packages}),
type: "POST",
contentType: "application/json",
@ -62,16 +79,33 @@
function removePackages() {
const onSuccess = update => `Packages ${update} have been removed`;
const onFailure = error => `Could not remove packages: ${error}`;
doPackageAction("/api/v1/service/remove", getSelection(), onSuccess, onFailure);
doPackageAction("/api/v1/service/remove", getSelection(), repository, onSuccess, onFailure);
}
function selectRepository() {
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
const element = $(`#${fragment}-lnk`);
element.click();
}
function updatePackages() {
const currentSelection = getSelection();
const [url, onSuccess] = currentSelection.length === 0
? ["/api/v1/service/update", _ => "Repository update has been run"]
? ["/api/v1/service/update", _ => `Repository update has been run`]
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
const onFailure = error => `Packages update failed: ${error}`;
doPackageAction(url, currentSelection, onSuccess, onFailure);
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
}
function updateAllPackages() {
const onSuccess = _ => "Repository update has been run";
const onFailure = error => `Packages update failed: ${error}`;
{% for repository in repositories %}
doPackageAction("/api/v1/service/update", [], {
architecture: "{{ repository.architecture }}",
repository: "{{ repository.repository }}",
}, onSuccess, onFailure);
{% endfor %}
}
function hideControls(hidden) {
@ -80,6 +114,7 @@
packageRebuildButton.attr("hidden", hidden);
packageRemoveButton.attr("hidden", hidden);
packageUpdateButton.attr("hidden", hidden);
packageUpdateAllButton.attr("hidden", hidden);
}
function reload() {
@ -95,6 +130,10 @@
$.ajax({
url: "/api/v1/packages",
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {
@ -149,10 +188,13 @@
$.ajax({
url: "/api/v1/status",
data: {
architecture: repository.architecture,
repository: repository.repository,
},
type: "GET",
dataType: "json",
success: response => {
repositoryBadge.text(`${response.repository} ${response.architecture}`);
versionBadge.html(`<i class="bi bi-github"></i> ahriman ${safe(response.version)}`);
statusBadge
@ -189,9 +231,17 @@
return extractDataList(table.bootstrapTable("getData"), "packager");
}
function getRepositorySelector(selector) {
const selected = selector.find(":selected");
return {
architecture: selected.data("architecture"),
repository: selected.data("repository"),
};
}
$(() => {
table.bootstrapTable({});
statusBadge.popover();
reload();
selectRepository();
});
</script>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="256.000000pt" height="256.000000pt" viewBox="0 0 256.000000 256.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,256.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1019 2546 c-51 -18 -92 -49 -136 -103 -34 -41 -83 -73 -112 -73 -6
0 4 22 20 49 17 27 30 54 28 59 -7 17 -153 -102 -176 -143 -13 -23 -30 -40
-40 -40 -15 0 -18 9 -18 61 0 49 6 73 33 125 l33 64 -42 -40 c-50 -49 -82 -97
-98 -150 -31 -102 -43 -125 -77 -146 -19 -11 -48 -42 -65 -67 -17 -26 -62 -76
-100 -112 -69 -66 -69 -66 -69 -120 0 -30 5 -62 10 -70 5 -8 10 -10 10 -4 0 7
21 35 46 63 69 77 154 95 154 33 0 -42 -27 -126 -50 -155 l-21 -26 25 -7 c34
-8 63 10 86 56 27 53 74 105 127 142 42 29 44 32 38 68 -6 33 -3 40 22 60 25
20 36 22 96 17 83 -7 121 5 137 43 7 16 19 31 28 34 10 3 -9 3 -40 1 -65 -6
-84 8 -48 37 34 27 118 24 180 -7 57 -29 115 -32 150 -8 l24 17 -24 6 c-14 3
-27 14 -28 24 -4 17 1 18 46 13 58 -7 118 -34 147 -67 38 -44 107 -80 149 -80
36 1 38 2 21 12 -41 23 -115 108 -115 133 0 13 58 -27 94 -64 42 -43 86 -58
133 -46 28 7 28 7 6 18 -13 5 -23 19 -23 29 0 14 6 19 23 16 17 -2 32 -23 62
-85 48 -99 63 -115 123 -141 97 -41 101 -40 50 16 -54 60 -80 113 -76 149 l3
26 58 -47 c61 -50 87 -89 87 -131 0 -43 27 -93 59 -109 38 -20 37 -21 36 11 0
45 24 38 47 -14 19 -41 22 -63 20 -146 -2 -91 0 -103 25 -148 45 -83 54 -83
47 2 -6 74 11 155 32 153 20 -2 47 -101 48 -179 1 -65 -3 -88 -22 -122 -24
-46 -28 -98 -10 -138 14 -30 24 -32 31 -5 8 30 37 25 37 -6 0 -53 -23 -113
-62 -160 -52 -63 -58 -81 -58 -167 l0 -72 35 65 c40 75 98 135 103 107 4 -19
-37 -114 -69 -158 -11 -16 -49 -46 -85 -66 -64 -37 -94 -76 -94 -122 0 -20 1
-21 24 -6 19 13 25 13 35 2 17 -21 -15 -43 -73 -51 -85 -11 -126 -26 -164 -61
-35 -31 -80 -95 -67 -95 3 0 29 12 57 26 52 26 168 45 168 28 0 -12 -92 -91
-123 -105 -20 -9 -34 -8 -71 6 -52 20 -104 14 -130 -14 -17 -19 -17 -20 6 -23
12 -2 24 -11 26 -20 4 -22 -37 -23 -189 -7 -99 11 -104 10 -151 -12 -26 -13
-48 -27 -48 -30 0 -3 25 -16 55 -28 30 -13 55 -26 55 -31 0 -8 -73 -20 -119
-20 -44 0 -99 20 -130 46 -37 31 -87 46 -124 38 l-27 -6 22 -18 c16 -13 20
-22 12 -29 -18 -19 -125 42 -174 100 -45 53 -95 82 -165 95 l-40 7 52 -41 c96
-76 114 -128 31 -86 -105 52 -120 69 -147 172 -9 31 -21 48 -44 62 -41 25 -61
26 -49 2 5 -9 7 -23 5 -29 -10 -27 -32 -11 -57 39 -21 43 -26 66 -26 132 0 71
-4 87 -29 131 -32 55 -80 106 -67 71 21 -58 18 -206 -4 -206 -5 0 -30 43 -56
95 l-48 96 17 47 c21 57 21 89 2 122 -14 25 -14 25 -15 3 0 -14 -6 -23 -16
-23 -14 0 -16 8 -10 53 4 28 18 70 31 92 14 22 31 55 40 73 18 37 32 152 19
152 -5 0 -17 -21 -28 -46 -18 -45 -61 -104 -75 -104 -4 0 -4 32 0 72 6 55 16
83 40 122 24 37 34 66 37 108 6 75 -1 91 -22 51 -16 -32 -48 -44 -61 -23 -8
12 22 69 40 76 19 8 41 43 65 105 l21 52 -29 -29 c-16 -16 -48 -40 -71 -54
-23 -14 -56 -45 -72 -69 -28 -43 -29 -47 -29 -175 0 -186 -12 -220 -43 -122
-10 29 -15 62 -11 72 4 14 -2 11 -20 -9 -31 -33 -33 -65 -7 -115 21 -40 38
-112 27 -112 -16 0 -58 35 -71 59 l-14 26 -1 -37 c0 -24 13 -63 36 -108 39
-76 51 -155 24 -155 -8 0 -20 16 -27 35 l-11 35 -7 -29 c-11 -40 11 -127 43
-175 15 -22 56 -72 90 -111 70 -78 66 -86 -28 -51 -71 27 -60 7 30 -54 71 -48
106 -93 86 -112 -4 -4 -46 12 -93 37 -110 58 -109 58 -87 20 29 -49 70 -81
188 -145 114 -63 148 -94 126 -120 -13 -16 -52 -6 -82 22 -28 25 -21 0 11 -44
45 -63 100 -83 259 -99 154 -15 221 -29 272 -54 l35 -18 -35 -6 c-56 -10 -164
-26 -175 -26 -5 -1 15 -10 45 -21 62 -23 124 -27 255 -15 72 6 94 5 109 -7 28
-21 11 -37 -42 -38 l-47 -1 40 -19 c63 -31 153 -22 244 25 80 41 166 66 224
66 l38 0 -26 -35 c-15 -19 -48 -48 -73 -66 l-47 -31 72 4 c71 4 72 4 130 59
66 64 104 86 160 95 27 4 42 2 50 -8 16 -19 4 -34 -43 -58 l-39 -19 54 -1 c71
0 129 28 181 88 22 26 68 80 102 120 35 41 74 89 87 108 13 19 26 33 28 31 2
-2 -3 -46 -12 -98 -20 -128 -20 -128 26 -73 52 63 64 91 83 196 18 108 39 150
69 146 23 -3 27 -20 14 -70 -11 -42 18 -25 49 28 34 58 41 139 20 222 -21 84
-21 83 19 172 19 43 38 99 41 125 l7 46 38 -68 39 -68 0 88 c0 80 -3 93 -35
157 -41 80 -47 140 -15 140 11 0 26 -12 34 -27 15 -27 15 -27 16 35 0 54 -4
70 -29 105 -16 23 -49 57 -74 76 -56 42 -111 153 -122 248 l-7 63 58 -26 c32
-14 71 -35 86 -45 40 -29 35 -10 -18 60 -34 45 -65 72 -117 103 -118 69 -153
102 -141 132 9 22 25 20 68 -9 42 -28 43 -26 14 26 -36 65 -136 119 -217 119
-48 0 -233 88 -211 100 8 4 44 11 80 14 129 14 130 15 58 47 -62 29 -64 29
-198 23 -121 -6 -138 -5 -153 11 -25 24 -6 43 46 47 l42 3 -35 20 c-25 13 -53
19 -98 19 -54 0 -74 -6 -143 -42 -76 -39 -139 -54 -139 -33 0 6 45 32 100 60
l99 50 -55 7 c-76 9 -111 2 -214 -46 -99 -46 -155 -51 -155 -15 0 14 12 24 44
36 40 14 42 16 20 22 -37 10 -80 8 -120 -7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -59,7 +59,7 @@
const [minDate, maxDate] = text.split(" - ");
const buildDate = asOfStartOfDay(new Date(value));
return (buildDate >= asOfStartOfDay(new Date(minDate))) && (buildDate <= asOfStartOfDay(new Date(maxDate)));
return (buildDate >= new Date(minDate)) && (buildDate <= new Date(maxDate));
}
function filterList(index, value, field, data) {

View File

@ -1,6 +1,6 @@
# AUTOMATICALLY GENERATED by `shtab`
_shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-repositories' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '-q' '--quiet' '--report' '--no-report' '-r' '--repository' '--unsafe' '--wait-timeout' '-V' '--version')
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
@ -50,14 +50,15 @@ _shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--depend
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_service_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_repo_config_option_strings=('-h' '--help' '--secure' '--no-secure')
_shtab_ahriman_service_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
_shtab_ahriman_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
_shtab_ahriman_repo_config_option_strings=('-h' '--help' '--info' '--no-info' '--secure' '--no-secure')
_shtab_ahriman_service_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_service_repositories_option_strings=('-h' '--help' '--id-only' '--no-id-only')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
@ -73,7 +74,7 @@ _shtab_ahriman_web_option_strings=('-h' '--help')
_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_pos_0_choices=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-repositories' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'service-tree-migrate' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman___log_handler_choices=('console' 'syslog' 'journald')
_shtab_ahriman_aur_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
_shtab_ahriman_search___sort_by_choices=('description' 'first_submitted' 'id' 'last_modified' 'maintainer' 'name' 'num_votes' 'out_of_date' 'package_base' 'package_base_id' 'popularity' 'repository' 'submitter' 'url' 'url_path' 'version')
@ -400,14 +401,20 @@ _shtab_ahriman_repo_clean___pacman_nargs=0
_shtab_ahriman_repo_clean___no_pacman_nargs=0
_shtab_ahriman_service_config__h_nargs=0
_shtab_ahriman_service_config___help_nargs=0
_shtab_ahriman_service_config___info_nargs=0
_shtab_ahriman_service_config___no_info_nargs=0
_shtab_ahriman_service_config___secure_nargs=0
_shtab_ahriman_service_config___no_secure_nargs=0
_shtab_ahriman_config__h_nargs=0
_shtab_ahriman_config___help_nargs=0
_shtab_ahriman_config___info_nargs=0
_shtab_ahriman_config___no_info_nargs=0
_shtab_ahriman_config___secure_nargs=0
_shtab_ahriman_config___no_secure_nargs=0
_shtab_ahriman_repo_config__h_nargs=0
_shtab_ahriman_repo_config___help_nargs=0
_shtab_ahriman_repo_config___info_nargs=0
_shtab_ahriman_repo_config___no_info_nargs=0
_shtab_ahriman_repo_config___secure_nargs=0
_shtab_ahriman_repo_config___no_secure_nargs=0
_shtab_ahriman_service_config_validate__h_nargs=0
@ -426,6 +433,10 @@ _shtab_ahriman_service_key_import__h_nargs=0
_shtab_ahriman_service_key_import___help_nargs=0
_shtab_ahriman_key_import__h_nargs=0
_shtab_ahriman_key_import___help_nargs=0
_shtab_ahriman_service_repositories__h_nargs=0
_shtab_ahriman_service_repositories___help_nargs=0
_shtab_ahriman_service_repositories___id_only_nargs=0
_shtab_ahriman_service_repositories___no_id_only_nargs=0
_shtab_ahriman_service_setup__h_nargs=0
_shtab_ahriman_service_setup___help_nargs=0
_shtab_ahriman_service_setup___generate_salt_nargs=0

View File

@ -1,16 +1,16 @@
.TH AHRIMAN "1" "2023\-09\-02" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2023\-10\-09" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [-q] [--report | --no-report] [-r REPOSITORY] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-repositories,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,service-tree-migrate,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
.SH OPTIONS
.TP
\fB\-a\fR \fI\,ARCHITECTURE\/\fR, \fB\-\-architecture\fR \fI\,ARCHITECTURE\/\fR
target architectures. For several subcommands it can be used multiple times
filter by target architecture
.TP
\fB\-c\fR \fI\,CONFIGURATION\/\fR, \fB\-\-configuration\fR \fI\,CONFIGURATION\/\fR
@ -38,7 +38,7 @@ force enable or disable reporting to web service
.TP
\fB\-r\fR \fI\,REPOSITORY\/\fR, \fB\-\-repository\fR \fI\,REPOSITORY\/\fR
target repository. For several subcommands it can be used multiple times
filter by target repository
.TP
\fB\-\-unsafe\fR
@ -155,6 +155,9 @@ validate system configuration
\fBahriman\fR \fI\,service\-key\-import\/\fR
import PGP key
.TP
\fBahriman\fR \fI\,service\-repositories\/\fR
show repositories
.TP
\fBahriman\fR \fI\,service\-setup\/\fR
initial service configuration
.TP
@ -664,11 +667,23 @@ clear directory with built packages
clear directory with pacman local database cache
.SH COMMAND \fI\,'ahriman service\-config'\/\fR
usage: ahriman service\-config [\-h] [\-\-secure | \-\-no\-secure]
usage: ahriman service\-config [\-h] [\-\-info | \-\-no\-info] [\-\-secure | \-\-no\-secure] [section] [key]
dump configuration for the specified architecture
.TP
\fBsection\fR
filter settings by section
.TP
\fBkey\fR
filter settings by key
.SH OPTIONS \fI\,'ahriman service\-config'\/\fR
.TP
\fB\-\-info\fR, \fB\-\-no\-info\fR
show additional information, e.g. configuration files
.TP
\fB\-\-secure\fR, \fB\-\-no\-secure\fR
hide passwords and secrets from output
@ -697,6 +712,16 @@ PGP key to import from public server
\fB\-\-key\-server\fR \fI\,KEY_SERVER\/\fR
key server for key import
.SH COMMAND \fI\,'ahriman service\-repositories'\/\fR
usage: ahriman service\-repositories [\-h] [\-\-id\-only | \-\-no\-id\-only]
list all available repositories
.SH OPTIONS \fI\,'ahriman service\-repositories'\/\fR
.TP
\fB\-\-id\-only\fR, \fB\-\-no\-id\-only\fR
show machine readable identifier instead
.SH COMMAND \fI\,'ahriman service\-setup'\/\fR
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-from\-configuration FROM_CONFIGURATION]
[\-\-generate\-salt | \-\-no\-generate\-salt] [\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs]

View File

@ -57,6 +57,7 @@ _shtab_ahriman_commands() {
"service-config:dump configuration for the specified architecture"
"service-config-validate:validate configuration and print found errors"
"service-key-import:import PGP key from public sources to the repository user"
"service-repositories:list all available repositories"
"service-setup:create initial service configuration, requires root"
"service-shell:drop into python shell"
"service-tree-migrate:migrate repository tree between versions"
@ -78,14 +79,14 @@ _shtab_ahriman_commands() {
_shtab_ahriman_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
"*"{-a,--architecture}"[target architectures. For several subcommands it can be used multiple times (default\: None)]:architecture:"
{-a,--architecture}"[filter by target architecture (default\: None)]:architecture:"
{-c,--configuration}"[configuration path (default\: \/etc\/ahriman.ini)]:configuration:"
"--force[force run, remove file lock (default\: False)]"
{-l,--lock}"[lock file (default\: \/tmp\/ahriman.lock)]:lock:"
"--log-handler[explicit log handler specification. If none set, the handler will be guessed from environment (default\: None)]:log_handler:(console syslog journald)"
{-q,--quiet}"[force disable any logging (default\: False)]"
{--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
"*"{-r,--repository}"[target repository. For several subcommands it can be used multiple times (default\: None)]:repository:"
{-r,--repository}"[filter by target repository (default\: None)]:repository:"
"--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable (default\: False)]"
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
"(- : *)"{-V,--version}"[show program\'s version number and exit]"
@ -130,7 +131,10 @@ _shtab_ahriman_clean_options=(
_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:"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: True)]:secure:"
":filter settings by section (default\: None):"
":filter settings by key (default\: None):"
)
_shtab_ahriman_config_validate_options=(
@ -310,7 +314,10 @@ _shtab_ahriman_repo_clean_options=(
_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:"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: True)]:secure:"
":filter settings by section (default\: None):"
":filter settings by key (default\: None):"
)
_shtab_ahriman_repo_config_validate_options=(
@ -457,7 +464,10 @@ _shtab_ahriman_service_clean_options=(
_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:"
{--secure,--no-secure}"[hide passwords and secrets from output (default\: True)]:secure:"
":filter settings by section (default\: None):"
":filter settings by key (default\: None):"
)
_shtab_ahriman_service_config_validate_options=(
@ -471,6 +481,11 @@ _shtab_ahriman_service_key_import_options=(
":PGP key to import from public server:"
)
_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:"
)
_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:"
@ -652,6 +667,7 @@ _shtab_ahriman() {
service-config) _arguments -C -s $_shtab_ahriman_service_config_options ;;
service-config-validate) _arguments -C -s $_shtab_ahriman_service_config_validate_options ;;
service-key-import) _arguments -C -s $_shtab_ahriman_service_key_import_options ;;
service-repositories) _arguments -C -s $_shtab_ahriman_service_repositories_options ;;
service-setup) _arguments -C -s $_shtab_ahriman_service_setup_options ;;
service-shell) _arguments -C -s $_shtab_ahriman_service_shell_options ;;
service-tree-migrate) _arguments -C -s $_shtab_ahriman_service_tree_migrate_options ;;

View File

@ -69,8 +69,7 @@ def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="ahriman", description="ArcH linux ReposItory MANager",
epilog="Argument list can also be read from file by using @ prefix.",
fromfile_prefix_chars="@", formatter_class=_formatter)
parser.add_argument("-a", "--architecture", help="target architectures. For several subcommands it can be used "
"multiple times", action="append")
parser.add_argument("-a", "--architecture", help="filter by target architecture")
parser.add_argument("-c", "--configuration", help="configuration path", type=Path,
default=Path("/etc") / "ahriman.ini")
parser.add_argument("--force", help="force run, remove file lock", action="store_true")
@ -82,8 +81,7 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("--report", help="force enable or disable reporting to web service",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-r", "--repository", help="target repository. For several subcommands it can be used "
"multiple times", action="append")
parser.add_argument("-r", "--repository", help="filter by target repository")
# special secret argument for systemd unit. The issue is that systemd doesn't allow multiple arguments to template
# name. This parameter accepts [[arch]-repo] in order to keep backward compatibility
parser.add_argument("--repository-id", help=argparse.SUPPRESS)
@ -130,6 +128,7 @@ def _parser() -> argparse.ArgumentParser:
_set_service_config_parser(subparsers)
_set_service_config_validate_parser(subparsers)
_set_service_key_import_parser(subparsers)
_set_service_repositories(subparsers)
_set_service_setup_parser(subparsers)
_set_service_shell_parser(subparsers)
_set_service_tree_migrate_parser(subparsers)
@ -161,8 +160,8 @@ def _set_aur_search_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--sort-by", help="sort field by this field. In case if two packages have the same value of "
"the specified field, they will be always sorted by name",
default="name", choices=sorted(handlers.Search.SORT_FIELDS))
parser.set_defaults(handler=handlers.Search, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Search, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True)
return parser
@ -180,7 +179,7 @@ def _set_help_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="show help message for application or command and exit",
formatter_class=_formatter)
parser.add_argument("command", help="show help message for specific command", nargs="?")
parser.set_defaults(handler=handlers.Help, architecture=[""], lock=None, quiet=True, report=False, repository=[""],
parser.set_defaults(handler=handlers.Help, architecture="", lock=None, quiet=True, report=False, repository="",
unsafe=True, parser=_parser)
return parser
@ -199,8 +198,8 @@ def _set_help_commands_unsafe_parser(root: SubParserAction) -> argparse.Argument
description="list unsafe commands as defined in default args", formatter_class=_formatter)
parser.add_argument("command", help="instead of showing commands, just test command line for unsafe subcommand "
"and return 0 in case if command is safe and 1 otherwise", nargs="*")
parser.set_defaults(handler=handlers.UnsafeCommands, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True, parser=_parser)
parser.set_defaults(handler=handlers.UnsafeCommands, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True, parser=_parser)
return parser
@ -218,8 +217,8 @@ def _set_help_updates_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="request AUR for current version and compare with current service version",
formatter_class=_formatter)
parser.add_argument("-e", "--exit-code", help="return non-zero exit code if updates available", action="store_true")
parser.set_defaults(handler=handlers.ServiceUpdates, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.ServiceUpdates, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True)
return parser
@ -235,8 +234,8 @@ def _set_help_version_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
parser = root.add_parser("help-version", aliases=["version"], help="application version",
description="print application and its dependencies versions", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Versions, architecture=[""], lock=None, quiet=True, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Versions, architecture="", lock=None, quiet=True, report=False,
repository="", unsafe=True)
return parser
@ -387,8 +386,8 @@ def _set_patch_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"it must end with ()")
parser.add_argument("patch", help="path to file which contains function or variable value. If not set, "
"the value will be read from stdin", type=Path, nargs="?")
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False,
repository=[""])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture="", lock=None, report=False,
repository="")
return parser
@ -408,8 +407,8 @@ def _set_patch_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-v", "--variable", help="if set, show only patches for specified PKGBUILD variables",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture=[""], lock=None, report=False,
repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Patch, action=Action.List, architecture="", lock=None, report=False,
repository="", unsafe=True)
return parser
@ -430,8 +429,8 @@ def _set_patch_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
"to remove only specified PKGBUILD variables. In case if not set, "
"it will remove all patches related to the package",
action="append")
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture=[""], lock=None, report=False,
repository=[""])
parser.set_defaults(handler=handlers.Patch, action=Action.Remove, architecture="", lock=None, report=False,
repository="")
return parser
@ -455,8 +454,8 @@ def _set_patch_set_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("package", help="path to directory with changed files for patch addition/update", type=Path)
parser.add_argument("-t", "--track", help="files which has to be tracked", action="append",
default=["*.diff", "*.patch"])
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture=[""], lock=None, report=False,
repository=[""], variable=None)
parser.set_defaults(handler=handlers.Patch, action=Action.Update, architecture="", lock=None, report=False,
repository="", variable=None)
return parser
@ -473,7 +472,7 @@ def _set_repo_backup_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("repo-backup", help="backup repository data",
description="backup repository settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the output archive", type=Path)
parser.set_defaults(handler=handlers.Backup, architecture=[""], lock=None, report=False, repository=[""],
parser.set_defaults(handler=handlers.Backup, architecture="", lock=None, report=False, repository="",
unsafe=True)
return parser
@ -653,7 +652,7 @@ def _set_repo_restore_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="restore settings and database", formatter_class=_formatter)
parser.add_argument("path", help="path of the input archive", type=Path)
parser.add_argument("-o", "--output", help="root path of the extracted files", type=Path, default=Path("/"))
parser.set_defaults(handler=handlers.Restore, architecture=[""], lock=None, report=False, repository=[""],
parser.set_defaults(handler=handlers.Restore, architecture="", lock=None, report=False, repository="",
unsafe=True)
return parser
@ -830,6 +829,10 @@ def _set_service_config_parser(root: SubParserAction) -> argparse.ArgumentParser
parser = root.add_parser("service-config", aliases=["config", "repo-config"], help="dump configuration",
description="dump configuration for the specified architecture",
formatter_class=_formatter)
parser.add_argument("section", help="filter settings by section", nargs="?")
parser.add_argument("key", help="filter settings by key", nargs="?")
parser.add_argument("--info", help="show additional information, e.g. configuration files",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--secure", help="hide passwords and secrets from output",
action=argparse.BooleanOptionalAction, default=True)
parser.set_defaults(handler=handlers.Dump, lock=None, quiet=True, report=False, unsafe=True)
@ -875,7 +878,27 @@ def _set_service_key_import_parser(root: SubParserAction) -> argparse.ArgumentPa
formatter_class=_formatter)
parser.add_argument("--key-server", help="key server for key import", default="keyserver.ubuntu.com")
parser.add_argument("key", help="PGP key to import from public server")
parser.set_defaults(handler=handlers.KeyImport, architecture=[""], lock=None, report=False, repository=[""])
parser.set_defaults(handler=handlers.KeyImport, architecture="", lock=None, report=False, repository="")
return parser
def _set_service_repositories(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for repositories listing
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("service-repositories", help="show repositories",
description="list all available repositories",
formatter_class=_formatter)
parser.add_argument("--id-only", help="show machine readable identifier instead",
action=argparse.BooleanOptionalAction, default=False)
parser.set_defaults(handler=handlers.Repositories, architecture="", lock=None, report=False, repository="",
unsafe=True)
return parser
@ -971,8 +994,8 @@ def _set_user_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
"which is in particular must be used for OAuth2 authorization type.")
parser.add_argument("-R", "--role", help="user access level",
type=UserAccess, choices=enum_values(UserAccess), default=UserAccess.Read)
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture=[""], lock=None, quiet=True,
report=False, repository=[""])
parser.set_defaults(handler=handlers.Users, action=Action.Update, architecture="", lock=None, quiet=True,
report=False, repository="")
return parser
@ -992,8 +1015,8 @@ def _set_user_list_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("username", help="filter users by username", nargs="?")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-R", "--role", help="filter users by role", type=UserAccess, choices=enum_values(UserAccess))
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture=[""], lock=None, quiet=True,
report=False, repository=[""], unsafe=True)
parser.set_defaults(handler=handlers.Users, action=Action.List, architecture="", lock=None, quiet=True,
report=False, repository="", unsafe=True)
return parser
@ -1011,8 +1034,8 @@ def _set_user_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
description="remove user from the user mapping and update the configuration",
formatter_class=_formatter)
parser.add_argument("username", help="username for web service")
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture=[""], lock=None, quiet=True,
report=False, repository=[""])
parser.set_defaults(handler=handlers.Users, action=Action.Remove, architecture="", lock=None, quiet=True,
report=False, repository="")
return parser
@ -1027,8 +1050,8 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("web", help="web server", description="start web server", formatter_class=_formatter)
parser.set_defaults(handler=handlers.Web, lock=Path(tempfile.gettempdir()) / "ahriman-web.lock", report=False,
parser=_parser)
parser.set_defaults(handler=handlers.Web, architecture="", lock=Path(tempfile.gettempdir()) / "ahriman-web.lock",
report=False, repository="", parser=_parser)
return parser

View File

@ -146,7 +146,7 @@ class Application(ApplicationPackages, ApplicationRepository):
# there is local cache, load package from it
package = Package.from_build(source_dir, self.repository.architecture, username)
else:
package = Package.from_aur(package_name, self.repository.pacman, username)
package = Package.from_aur(package_name, username)
with_dependencies[package.base] = package
# register package in local database

View File

@ -63,7 +63,7 @@ class ApplicationPackages(ApplicationProperties):
source(str): package base name
username(str | None): optional override of username for build process
"""
package = Package.from_aur(source, self.repository.pacman, username)
package = Package.from_aur(source, username)
self.database.build_queue_insert(package)
self.database.remote_update(package)

View File

@ -104,7 +104,7 @@ class ApplicationRepository(ApplicationProperties):
packages: list[str] = []
for single in probe.packages:
try:
_ = Package.from_aur(single, self.repository.pacman, None)
_ = Package.from_aur(single, None)
except Exception:
packages.append(single)
return packages

View File

@ -30,6 +30,7 @@ from ahriman.application.handlers.patch import Patch
from ahriman.application.handlers.rebuild import Rebuild
from ahriman.application.handlers.remove import Remove
from ahriman.application.handlers.remove_unknown import RemoveUnknown
from ahriman.application.handlers.repositories import Repositories
from ahriman.application.handlers.restore import Restore
from ahriman.application.handlers.search import Search
from ahriman.application.handlers.service_updates import ServiceUpdates

View File

@ -44,12 +44,18 @@ class Dump(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
root, _ = configuration.check_loaded()
ConfigurationPathsPrinter(root, configuration.includes)(verbose=True, separator=" = ")
if args.info:
root, _ = configuration.check_loaded()
ConfigurationPathsPrinter(root, configuration.includes)(verbose=True, separator=" = ")
# empty line
StringPrinter("")(verbose=False)
dump = configuration.dump()
for section, values in sorted(dump.items()):
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
match (args.section, args.key):
case None, None: # full configuration
dump = configuration.dump()
for section, values in sorted(dump.items()):
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
case section, None: # section only
values = dict(configuration.items(section)) if configuration.has_section(section) else {}
ConfigurationPrinter(section, values)(verbose=not args.secure, separator=" = ")
case section, key: # key only
value = configuration.get(section, key, fallback="")
StringPrinter(value)(verbose=False)

View File

@ -20,6 +20,7 @@
import argparse
import logging
from collections.abc import Iterable
from multiprocessing import Pool
from ahriman.application.lock import Lock
@ -64,7 +65,7 @@ class Handler:
configuration = Configuration.from_path(args.configuration, repository_id)
log_handler = LogLoader.handler(args.log_handler)
LogLoader.load(configuration, log_handler, quiet=args.quiet, report=args.report)
LogLoader.load(repository_id, configuration, log_handler, quiet=args.quiet, report=args.report)
with Lock(args, repository_id, configuration):
cls.run(args, repository_id, configuration, report=args.report)
@ -105,59 +106,6 @@ class Handler:
return 0 if all(result) else 1
@classmethod
def repositories_extract(cls, args: argparse.Namespace) -> list[RepositoryId]:
"""
get known architectures
Args:
args(argparse.Namespace): command line args
Returns:
list[RepositoryId]: list of repository names and architectures for which tree is created
Raises:
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
configuration = Configuration()
configuration.load(args.configuration)
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# preparse systemd repository-id argument
# we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes
if args.repository_id is not None:
# repository parts is optional for backward compatibility
architecture, *repository_parts = args.repository_id.split("/")
args.architecture = [architecture]
if repository_parts:
args.repository = ["-".join(repository_parts)] # replace slash with dash
# extract repository names first
names = args.repository
if names is None: # try to read file system first
names = RepositoryPaths.known_repositories(root)
if not names: # try to read configuration now
names = [configuration.get("repository", "name")]
# extract architecture names
if (architectures := args.architecture) is not None:
repositories = set(
RepositoryId(architecture, name)
for name in names
for architecture in architectures
)
else: # try to read from file system
repositories = set(
RepositoryId(architecture, name)
for name in names
for architecture in RepositoryPaths.known_architectures(root, name)
)
if not repositories:
raise MissingArchitectureError(args.command)
return sorted(repositories)
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
@ -189,3 +137,57 @@ class Handler:
"""
if enabled and predicate:
raise ExitCode
@staticmethod
def repositories_extract(args: argparse.Namespace) -> list[RepositoryId]:
"""
get known architectures
Args:
args(argparse.Namespace): command line args
Returns:
list[RepositoryId]: list of repository names and architectures for which tree is created
Raises:
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
configuration = Configuration()
configuration.load(args.configuration)
# pylint, wtf???
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
# preparse systemd repository-id argument
# we are using unescaped values, so / is not allowed here, because it is impossible to separate if from dashes
if args.repository_id is not None:
separator = "/" if "/" in args.repository_id else "-" # systemd and non-systemd identifiers
# repository parts is optional for backward compatibility
architecture, *repository_parts = args.repository_id.split(separator)
args.architecture = architecture
if repository_parts:
args.repository = "-".join(repository_parts) # replace slash with dash
# extract repository names first
if (from_args := args.repository) is not None:
repositories: Iterable[str] = [from_args]
elif (from_filesystem := RepositoryPaths.known_repositories(root)):
repositories = from_filesystem
else: # try to read configuration now
repositories = [configuration.get("repository", "name")]
# extract architecture names
if (architecture := args.architecture) is not None:
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
)
else: # try to read from file system
parsed = set(
RepositoryId(architecture, repository)
for repository in repositories
for architecture in RepositoryPaths.known_architectures(root, repository)
)
if not parsed:
raise MissingArchitectureError(args.command)
return sorted(parsed)

View File

@ -0,0 +1,54 @@
#
# Copyright (c) 2021-2023 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 argparse
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import RepositoryPrinter
from ahriman.models.repository_id import RepositoryId
class Repositories(Handler):
"""
repositories listing handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
report: bool) -> None:
"""
callback for command line
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
dummy_args = argparse.Namespace(
architecture=None,
configuration=args.configuration,
repository=None,
repository_id=None,
)
for repository in cls.repositories_extract(dummy_args):
RepositoryPrinter(repository)(verbose=not args.id_only)

View File

@ -22,7 +22,6 @@ import argparse
from dataclasses import fields
from collections.abc import Callable, Iterable
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.alpm.remote import AUR, Official
from ahriman.core.configuration import Configuration
@ -59,10 +58,8 @@ class Search(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
official_packages_list = Official.multisearch(*args.search, pacman=application.repository.pacman)
aur_packages_list = AUR.multisearch(*args.search, pacman=application.repository.pacman)
official_packages_list = Official.multisearch(*args.search)
aur_packages_list = AUR.multisearch(*args.search)
Search.check_if_empty(args.exit_code, not official_packages_list and not aur_packages_list)
for packages_list in (official_packages_list, aur_packages_list):

View File

@ -20,7 +20,6 @@
import argparse
from ahriman import __version__
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import UpdatePrinter
@ -47,9 +46,7 @@ class ServiceUpdates(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
application = Application(repository_id, configuration, report=report)
remote = Package.from_aur("ahriman", application.repository.pacman, None)
remote = Package.from_aur("ahriman", None)
_, release = remote.version.rsplit("-", 1) # we don't store pkgrel locally, so we just append it
local_version = f"{__version__}-{release}"

View File

@ -126,16 +126,16 @@ class Setup(Handler):
configuration.set_option(section, "target", " ".join([target.name.lower() for target in sign_targets]))
configuration.set_option(section, "key", args.sign_key)
section = Configuration.section_name("web", repository_id.name, repository_id.architecture)
if args.web_port is not None:
configuration.set_option(section, "port", str(args.web_port))
configuration.set_option("web", "port", str(args.web_port))
if args.web_unix_socket is not None:
configuration.set_option(section, "unix_socket", str(args.web_unix_socket))
configuration.set_option("web", "unix_socket", str(args.web_unix_socket))
if args.generate_salt:
configuration.set_option("auth", "salt", User.generate_password(20))
target = root.include / "00-setup-overrides.ini"
(root.include / "00-setup-overrides.ini").unlink(missing_ok=True) # remove old-style configuration
target = root.repository_paths.root / f"00-setup-overrides-{repository_id.id}.ini"
with target.open("w") as ahriman_configuration:
configuration.write(ahriman_configuration)

View File

@ -32,7 +32,7 @@ class Web(Handler):
web server handler
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
@ -47,13 +47,20 @@ class Web(Handler):
report(bool): force enable or disable reporting
"""
# we are using local import for optional dependencies
from ahriman.web.web import run_server, setup_service
from ahriman.web.web import run_server, setup_server
spawner_args = Web.extract_arguments(args, repository_id, configuration)
spawner = Spawn(args.parser(), repository_id, list(spawner_args))
spawner_args = Web.extract_arguments(args, configuration)
spawner = Spawn(args.parser(), list(spawner_args))
spawner.start()
application = setup_service(repository_id, configuration, spawner)
dummy_args = argparse.Namespace(
architecture=None,
configuration=args.configuration,
repository=None,
repository_id=None,
)
repositories = cls.repositories_extract(dummy_args)
application = setup_server(configuration, spawner, repositories)
run_server(application)
# terminate spawn process at the last
@ -61,22 +68,17 @@ class Web(Handler):
spawner.join()
@staticmethod
def extract_arguments(args: argparse.Namespace, repository_id: RepositoryId,
configuration: Configuration) -> Generator[str, None, None]:
def extract_arguments(args: argparse.Namespace, configuration: Configuration) -> Generator[str, None, None]:
"""
extract list of arguments used for current command, except for command specific ones
Args:
args(argparse.Namespace): command line args
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
Returns:
Generator[str, None, None]: command line arguments which were used for this specific command
"""
# read architecture from the same argument list
yield from ["--architecture", repository_id.architecture]
yield from ["--repository", repository_id.name]
# read configuration path from current settings
if (configuration_path := configuration.path) is not None:
yield from ["--configuration", str(configuration_path)]

View File

@ -79,7 +79,7 @@ class Lock(LazyLogging):
self.wait_timeout: int = args.wait_timeout
self.paths = configuration.repository_paths
self.reporter = Client.load(configuration, report=args.report)
self.reporter = Client.load(repository_id, configuration, report=args.report)
def check_version(self) -> None:
"""

View File

@ -113,13 +113,13 @@ class AUR(Remote):
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -133,13 +133,13 @@ class AUR(Remote):
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -107,13 +107,13 @@ class Official(Remote):
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -127,13 +127,13 @@ class Official(Remote):
except StopIteration:
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -38,13 +38,13 @@ class OfficialSyncdb(Official):
Still we leave search function based on the official repositories RPC.
"""
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -52,6 +52,9 @@ class OfficialSyncdb(Official):
Raises:
UnknownPackageError: package doesn't exist
"""
if pacman is None:
raise UnknownPackageError(package_name)
try:
return next(AURPackage.from_pacman(package) for package in pacman.package_get(package_name))
except StopIteration:

View File

@ -40,13 +40,14 @@ class Remote(SyncHttpClient):
"""
@classmethod
def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage:
def info(cls, package_name: str, *, pacman: Pacman | None = None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
Returns:
AURPackage: package which match the package name
@ -54,14 +55,15 @@ class Remote(SyncHttpClient):
return cls().package_info(package_name, pacman=pacman)
@classmethod
def multisearch(cls, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def multisearch(cls, *keywords: str, pacman: Pacman | 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
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
Returns:
list[AURPackage]: list of packages each of them matches all search terms
@ -111,26 +113,27 @@ class Remote(SyncHttpClient):
raise NotImplementedError
@classmethod
def search(cls, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def search(cls, *keywords: str, pacman: Pacman | None = None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): search terms, e.g. "ahriman", "is", "cool"
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None, optional): alpm wrapper instance, required for official repositories search
(Default value = None)
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return cls().package_search(*keywords, pacman=pacman)
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
def package_info(self, package_name: str, *, pacman: Pacman | None) -> AURPackage:
"""
get package info by its name
Args:
package_name(str): package name to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
AURPackage: package which match the package name
@ -140,13 +143,13 @@ class Remote(SyncHttpClient):
"""
raise NotImplementedError
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
def package_search(self, *keywords: str, pacman: Pacman | None) -> list[AURPackage]:
"""
search package in AUR web
Args:
*keywords(str): keywords to search
pacman(Pacman): alpm wrapper instance
pacman(Pacman | None): alpm wrapper instance, required for official repositories search
Returns:
list[AURPackage]: list of packages which match the criteria

View File

@ -232,7 +232,7 @@ class Configuration(configparser.RawConfigParser):
dict[str, dict[str, str]]: configuration dump for specific architecture
"""
return {
section: dict(self[section])
section: dict(self.items(section))
for section in self.sections()
}
@ -282,20 +282,27 @@ class Configuration(configparser.RawConfigParser):
if not path.is_file(): # fallback to the system file
path = self.SYSTEM_CONFIGURATION_PATH
self.path = path
self.read(self.path)
self.load_includes()
def load_includes(self) -> None:
"""
load configuration includes
"""
self.read(self.path)
self.includes = [] # reset state
self.load_includes() # basic includes
self.load_includes(self.getpath("repository", "root")) # home directory includes
def load_includes(self, path: Path | None = None) -> None:
"""
load configuration includes from specified path
Args:
path(Path | None, optional): path to directory with include files (Default value = None)
"""
try:
for path in sorted(self.include.glob("*.ini")):
if path == self.logging_path:
path = path or self.include
for include in sorted(path.glob("*.ini")):
if include == self.logging_path:
continue # we don't want to load logging explicitly
self.read(path)
self.includes.append(path)
self.read(include)
self.includes.append(include)
except (FileNotFoundError, configparser.NoOptionError, configparser.NoSectionError):
pass

View File

@ -42,7 +42,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"include": {
"type": "path",
"coerce": "absolute_path",
"required": True,
"path_exists": True,
"path_type": "dir",
},

View File

@ -43,7 +43,7 @@ class BuildOperations(Operations):
""",
{
"package_base": package_base,
"repository": self.repository_id.id,
"repository": self._repository_id.id,
})
return self.with_connection(run, commit=True)
@ -60,7 +60,7 @@ class BuildOperations(Operations):
Package.from_json(row["properties"])
for row in connection.execute(
"""select properties from build_queue where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": self._repository_id.id}
)
]
@ -86,7 +86,7 @@ class BuildOperations(Operations):
{
"package_base": package.base,
"properties": package.view(),
"repository": self.repository_id.id,
"repository": self._repository_id.id,
})
return self.with_connection(run, commit=True)

View File

@ -21,6 +21,7 @@ from sqlite3 import Connection
from ahriman.core.database.operations import Operations
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.repository_id import RepositoryId
class LogsOperations(Operations):
@ -28,7 +29,8 @@ class LogsOperations(Operations):
logs operations
"""
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
repository_id: RepositoryId | None = None) -> list[tuple[float, str]]:
"""
extract logs for specified package base
@ -36,10 +38,13 @@ class LogsOperations(Operations):
package_base(str): package base to extract logs
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Return:
list[tuple[float, str]]: sorted package log records and their timestamps
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> list[tuple[float, str]]:
return [
(row["created"], row["record"])
@ -51,7 +56,7 @@ class LogsOperations(Operations):
""",
{
"package_base": package_base,
"repository": self.repository_id.id,
"repository": repository_id.id,
"limit": limit,
"offset": offset,
})
@ -59,7 +64,8 @@ class LogsOperations(Operations):
return self.with_connection(run)
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str) -> None:
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str,
repository_id: RepositoryId | None = None) -> None:
"""
write new log record to database
@ -67,7 +73,10 @@ class LogsOperations(Operations):
log_record_id(LogRecordId): current log record id
created(float): log created timestamp from log record attribute
record(str): log record
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
@ -81,21 +90,23 @@ class LogsOperations(Operations):
"version": log_record_id.version,
"created": created,
"record": record,
"repository": self.repository_id.id,
"repository": repository_id.id,
}
)
return self.with_connection(run, commit=True)
def logs_remove(self, package_base: str, version: str | None) -> None:
def logs_remove(self, package_base: str, version: str | None, repository_id: RepositoryId | None = None) -> None:
"""
remove log records for the specified package
Args:
package_base(str): package base to remove logs
version(str): package version. If set it will remove only logs belonging to another
version
version(str | None): package version. If set it will remove only logs belonging to another version
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
connection.execute(
"""
@ -107,7 +118,7 @@ class LogsOperations(Operations):
{
"package_base": package_base,
"version": version,
"repository": self.repository_id.id,
"repository": repository_id.id,
}
)

View File

@ -36,7 +36,6 @@ class Operations(LazyLogging):
Attributes:
path(Path): path to the database file
repository_id(RepositoryId): repository unique identifier to perform implicit filtering
"""
def __init__(self, path: Path, repository_id: RepositoryId) -> None:
@ -48,7 +47,7 @@ class Operations(LazyLogging):
repository_id(RepositoryId): repository unique identifier
"""
self.path = path
self.repository_id = repository_id
self._repository_id = repository_id
@staticmethod
def factory(cursor: sqlite3.Cursor, row: tuple[Any, ...]) -> dict[str, Any]:

View File

@ -25,6 +25,7 @@ from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.package_description import PackageDescription
from ahriman.models.remote_source import RemoteSource
from ahriman.models.repository_id import RepositoryId
class PackageOperations(Operations):
@ -32,23 +33,25 @@ class PackageOperations(Operations):
package operations
"""
def _package_remove_package_base(self, connection: Connection, package_base: str) -> None:
def _package_remove_package_base(self, connection: Connection, package_base: str,
repository_id: RepositoryId) -> None:
"""
remove package base information
Args:
connection(Connection): database connection
package_base(str): package base name
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""delete from package_statuses where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": self.repository_id.id})
{"package_base": package_base, "repository": repository_id.id})
connection.execute(
"""delete from package_bases where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": self.repository_id.id})
{"package_base": package_base, "repository": repository_id.id})
def _package_remove_packages(self, connection: Connection, package_base: str,
current_packages: Iterable[str]) -> None:
current_packages: Iterable[str], repository_id: RepositoryId) -> None:
"""
remove packages belong to the package base
@ -56,6 +59,7 @@ class PackageOperations(Operations):
connection(Connection): database connection
package_base(str): package base name
current_packages(Iterable[str]): current packages list which has to be left in database
repository_id(RepositoryId): repository unique identifier
"""
packages = [
package
@ -63,20 +67,22 @@ class PackageOperations(Operations):
"""
select package, repository from packages
where package_base = :package_base and repository = :repository""",
{"package_base": package_base, "repository": self.repository_id.id})
{"package_base": package_base, "repository": repository_id.id})
if package["package"] not in current_packages
]
connection.executemany(
"""delete from packages where package = :package and repository = :repository""",
packages)
def _package_update_insert_base(self, connection: Connection, package: Package) -> None:
def _package_update_insert_base(self, connection: Connection, package: Package,
repository_id: RepositoryId) -> None:
"""
insert base package into table
Args:
connection(Connection): database connection
package(Package): package properties
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""
@ -97,17 +103,19 @@ class PackageOperations(Operations):
"web_url": package.remote.web_url,
"source": package.remote.source.value,
"packager": package.packager,
"repository": self.repository_id.id,
"repository": repository_id.id,
}
)
def _package_update_insert_packages(self, connection: Connection, package: Package) -> None:
def _package_update_insert_packages(self, connection: Connection, package: Package,
repository_id: RepositoryId) -> None:
"""
insert packages into table
Args:
connection(Connection): database connection
package(Package): package properties
repository_id(RepositoryId): repository unique identifier
"""
package_list = []
for name, description in package.packages.items():
@ -116,7 +124,7 @@ class PackageOperations(Operations):
package_list.append({
"package": name,
"package_base": package.base,
"repository": self.repository_id.id,
"repository": repository_id.id,
**description.view(),
})
connection.executemany(
@ -141,7 +149,8 @@ class PackageOperations(Operations):
""",
package_list)
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus) -> None:
def _package_update_insert_status(self, connection: Connection, package_base: str, status: BuildStatus,
repository_id: RepositoryId) -> None:
"""
insert base package status into table
@ -149,6 +158,7 @@ class PackageOperations(Operations):
connection(Connection): database connection
package_base(str): package base name
status(BuildStatus): new build status
repository_id(RepositoryId): repository unique identifier
"""
connection.execute(
"""
@ -163,15 +173,17 @@ class PackageOperations(Operations):
"package_base": package_base,
"status": status.status.value,
"last_updated": status.timestamp,
"repository": self.repository_id.id,
"repository": repository_id.id,
})
def _packages_get_select_package_bases(self, connection: Connection) -> dict[str, Package]:
def _packages_get_select_package_bases(self, connection: Connection,
repository_id: RepositoryId) -> dict[str, Package]:
"""
select package bases from the table
Args:
connection(Connection): database connection
repository_id(RepositoryId): repository unique identifier
Returns:
dict[str, Package]: map of the package base to its descriptor (without packages themselves)
@ -185,36 +197,40 @@ class PackageOperations(Operations):
packager=row["packager"] or None,
) for row in connection.execute(
"""select * from package_bases where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
)
}
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package]) -> dict[str, Package]:
def _packages_get_select_packages(self, connection: Connection, packages: dict[str, Package],
repository_id: RepositoryId) -> dict[str, Package]:
"""
select packages from the table
Args:
connection(Connection): database connection
packages(dict[str, Package]): packages descriptor map
repository_id(RepositoryId): repository unique identifier
Returns:
dict[str, Package]: map of the package base to its descriptor including individual packages
"""
for row in connection.execute(
"""select * from packages where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
):
if row["package_base"] not in packages:
continue # normally must never happen though
packages[row["package_base"]].packages[row["package"]] = PackageDescription.from_json(row)
return packages
def _packages_get_select_statuses(self, connection: Connection) -> dict[str, BuildStatus]:
def _packages_get_select_statuses(self, connection: Connection,
repository_id: RepositoryId) -> dict[str, BuildStatus]:
"""
select package build statuses from the table
Args:
connection(Connection): database connection
repository_id(RepositoryId): repository unique identifier
Returns:
dict[str, BuildStatus]: map of the package base to its status
@ -223,50 +239,62 @@ class PackageOperations(Operations):
row["package_base"]: BuildStatus.from_json({"status": row["status"], "timestamp": row["last_updated"]})
for row in connection.execute(
"""select * from package_statuses where repository = :repository""",
{"repository": self.repository_id.id}
{"repository": repository_id.id}
)
}
def package_remove(self, package_base: str) -> None:
def package_remove(self, package_base: str, repository_id: RepositoryId | None = None) -> None:
"""
remove package from database
Args:
package_base(str): package base name
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
self._package_remove_packages(connection, package_base, [])
self._package_remove_package_base(connection, package_base)
self._package_remove_packages(connection, package_base, [], repository_id)
self._package_remove_package_base(connection, package_base, repository_id)
return self.with_connection(run, commit=True)
def package_update(self, package: Package, status: BuildStatus) -> None:
def package_update(self, package: Package, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
"""
update package status
Args:
package(Package): package properties
status(BuildStatus): new build status
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> None:
self._package_update_insert_base(connection, package)
self._package_update_insert_status(connection, package.base, status)
self._package_update_insert_packages(connection, package)
self._package_remove_packages(connection, package.base, package.packages.keys())
self._package_update_insert_base(connection, package, repository_id)
self._package_update_insert_status(connection, package.base, status, repository_id)
self._package_update_insert_packages(connection, package, repository_id)
self._package_remove_packages(connection, package.base, package.packages.keys(), repository_id)
return self.with_connection(run, commit=True)
def packages_get(self) -> list[tuple[Package, BuildStatus]]:
def packages_get(self, repository_id: RepositoryId | None = None) -> list[tuple[Package, BuildStatus]]:
"""
get package list and their build statuses from database
Args:
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Return:
list[tuple[Package, BuildStatus]]: list of package properties and their statuses
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> Generator[tuple[Package, BuildStatus], None, None]:
packages = self._packages_get_select_package_bases(connection)
statuses = self._packages_get_select_statuses(connection)
for package_base, package in self._packages_get_select_packages(connection, packages).items():
packages = self._packages_get_select_package_bases(connection, repository_id)
statuses = self._packages_get_select_statuses(connection, repository_id)
per_package_base = self._packages_get_select_packages(connection, packages, repository_id)
for package_base, package in per_package_base.items():
yield package, statuses.get(package_base, BuildStatus())
return self.with_connection(lambda connection: list(run(connection)))
@ -279,7 +307,7 @@ class PackageOperations(Operations):
package(Package): package properties
"""
return self.with_connection(
lambda connection: self._package_update_insert_base(connection, package),
lambda connection: self._package_update_insert_base(connection, package, self._repository_id),
commit=True)
def remotes_get(self) -> dict[str, RemoteSource]:
@ -289,8 +317,10 @@ class PackageOperations(Operations):
Returns:
dict[str, RemoteSource]: map of package base to its remote sources
"""
packages = self.with_connection(self._packages_get_select_package_bases)
def run(connection: Connection) -> dict[str, Package]:
return self._packages_get_select_package_bases(connection, self._repository_id)
return {
package_base: package.remote
for package_base, package in packages.items()
for package_base, package in self.with_connection(run).items()
}

View File

@ -26,6 +26,7 @@ from ahriman.core.formatters.configuration_paths_printer import ConfigurationPat
from ahriman.core.formatters.configuration_printer import ConfigurationPrinter
from ahriman.core.formatters.package_printer import PackagePrinter
from ahriman.core.formatters.patch_printer import PatchPrinter
from ahriman.core.formatters.repository_printer import RepositoryPrinter
from ahriman.core.formatters.status_printer import StatusPrinter
from ahriman.core.formatters.tree_printer import TreePrinter
from ahriman.core.formatters.update_printer import UpdatePrinter

View File

@ -0,0 +1,53 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.formatters import StringPrinter
from ahriman.models.property import Property
from ahriman.models.repository_id import RepositoryId
class RepositoryPrinter(StringPrinter):
"""
print repository unique identifier
Attributes:
repository_id(RepositoryId): repository unique identifier
"""
def __init__(self, repository_id: RepositoryId) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
"""
StringPrinter.__init__(self, repository_id.id)
self.repository_id = repository_id
def properties(self) -> list[Property]:
"""
convert content into printable data
Returns:
list[Property]: list of content properties
"""
return [
Property("Name", self.repository_id.name),
Property("Architecture", self.repository_id.architecture),
]

View File

@ -22,6 +22,7 @@ import logging
from typing import Self
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
class HttpLogHandler(logging.Handler):
@ -34,11 +35,13 @@ class HttpLogHandler(logging.Handler):
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
def __init__(self, configuration: Configuration, *, report: bool, suppress_errors: bool) -> None:
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
report: bool, suppress_errors: bool) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
@ -48,16 +51,17 @@ class HttpLogHandler(logging.Handler):
# client has to be imported here because of circular imports
from ahriman.core.status.client import Client
self.reporter = Client.load(configuration, report=report)
self.reporter = Client.load(repository_id, configuration, report=report)
self.suppress_errors = suppress_errors
@classmethod
def load(cls, configuration: Configuration, *, report: bool) -> Self:
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
"""
install logger. This function creates handler instance and adds it to the handler list in case if no other
http handler found
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
@ -69,7 +73,7 @@ class HttpLogHandler(logging.Handler):
return handler # there is already registered instance
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
handler = cls(configuration, report=report, suppress_errors=suppress_errors)
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
root.addHandler(handler)
return handler

View File

@ -25,6 +25,7 @@ from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler
from ahriman.models.log_handler import LogHandler
from ahriman.models.repository_id import RepositoryId
class LogLoader:
@ -68,11 +69,13 @@ class LogLoader:
return LogHandler.Console
@staticmethod
def load(configuration: Configuration, handler: LogHandler, *, quiet: bool, report: bool) -> None:
def load(repository_id: RepositoryId, configuration: Configuration, handler: LogHandler, *,
quiet: bool, report: bool) -> None:
"""
setup logging settings from configuration
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
handler(LogHandler): selected default log handler, which will be used if no handlers were set
quiet(bool): force disable any log messages
@ -97,7 +100,7 @@ class LogLoader:
logging.basicConfig(filename=None, format=LogLoader.DEFAULT_LOG_FORMAT, level=LogLoader.DEFAULT_LOG_LEVEL)
logging.exception("could not load logging from configuration, fallback to stderr")
HttpLogHandler.load(configuration, report=report)
HttpLogHandler.load(repository_id, configuration, report=report)
if quiet:
logging.disable(logging.WARNING) # only print errors here

View File

@ -51,7 +51,7 @@ class RemoteCall(Report):
"""
Report.__init__(self, repository_id, configuration)
self.client = WebClient(configuration)
self.client = WebClient(repository_id, configuration)
self.update_aur = configuration.getboolean(section, "aur", fallback=False)
self.update_local = configuration.getboolean(section, "local", fallback=False)
@ -100,11 +100,13 @@ class RemoteCall(Report):
Returns:
str: remote process id
"""
response = self.client.make_request("POST", "/api/v1/service/update", json={
"aur": self.update_aur,
"local": self.update_local,
"manual": self.update_manual,
})
response = self.client.make_request("POST", "/api/v1/service/update",
params=self.repository_id.query(),
json={
"aur": self.update_aur,
"local": self.update_local,
"manual": self.update_manual,
})
response_json = response.json()
process_id: str = response_json["process_id"]

View File

@ -75,7 +75,7 @@ class RepositoryProperties(LazyLogging):
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
self.sign = GPG(configuration)
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
self.reporter = Client.load(configuration, report=report)
self.reporter = Client.load(repository_id, configuration, report=report)
self.triggers = TriggerLoader.load(repository_id, configuration)
@property

View File

@ -61,7 +61,7 @@ class UpdateHandler(Cleaner):
try:
if package.remote.source == PackageSource.Repository:
return Package.from_official(probe, self.pacman, None)
return Package.from_aur(probe, self.pacman, None)
return Package.from_aur(probe, None)
except UnknownPackageError:
continue
raise UnknownPackageError(package.base)

View File

@ -28,6 +28,7 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread
from ahriman.core.log import LazyLogging
from ahriman.models.process_status import ProcessStatus
from ahriman.models.repository_id import RepositoryId
@ -39,22 +40,18 @@ class Spawn(Thread, LazyLogging):
Attributes:
active(dict[str, Process]): map of active child processes required to avoid zombies
command_arguments(list[str]): base command line arguments
queue(Queue[tuple[str, bool, int]]): multiprocessing queue to read updates from processes
repository_id(RepositoryId): repository unique identifier
queue(Queue[ProcessStatus | None]): multiprocessing queue to read updates from processes
"""
def __init__(self, args_parser: argparse.ArgumentParser, repository_id: RepositoryId,
command_arguments: list[str]) -> None:
def __init__(self, args_parser: argparse.ArgumentParser, command_arguments: list[str]) -> None:
"""
default constructor
Args:
args_parser(argparse.ArgumentParser): command line parser for the application
repository_id(RepositoryId): repository unique identifier
command_arguments(list[str]): base command line arguments
"""
Thread.__init__(self, name="spawn")
self.repository_id = repository_id
self.args_parser = args_parser
self.command_arguments = command_arguments
@ -62,7 +59,7 @@ class Spawn(Thread, LazyLogging):
self.lock = Lock()
self.active: dict[str, Process] = {}
# stupid pylint does not know that it is possible
self.queue: Queue[tuple[str, bool, int] | None] = Queue() # pylint: disable=unsubscriptable-object
self.queue: Queue[ProcessStatus | None] = Queue() # pylint: disable=unsubscriptable-object
@staticmethod
def boolean_action_argument(name: str, value: bool) -> str:
@ -80,16 +77,16 @@ class Spawn(Thread, LazyLogging):
@staticmethod
def process(callback: Callable[[argparse.Namespace, RepositoryId], bool], args: argparse.Namespace,
repository_id: RepositoryId, process_id: str, queue: Queue[tuple[str, bool, int]]) -> None: # pylint: disable=unsubscriptable-object
repository_id: RepositoryId, process_id: str, queue: Queue[ProcessStatus | None]) -> None: # pylint: disable=unsubscriptable-object
"""
helper to run external process
Args:
callback(Callable[[argparse.Namespace, str], bool]): application run function (i.e. Handler.run method)
callback(Callable[[argparse.Namespace, str], bool]): application run function (i.e. ``Handler.call`` method)
args(argparse.Namespace): command line arguments
repository_id(RepositoryId): repository unique identifier
process_id(str): process unique identifier
queue(Queue[tuple[str, bool, int]]): output queue
queue(Queue[ProcessStatus | None]): output queue
"""
start_time = time.monotonic()
result = callback(args, repository_id)
@ -97,19 +94,20 @@ class Spawn(Thread, LazyLogging):
consumed_time = int(1000 * (stop_time - start_time))
queue.put((process_id, result, consumed_time))
queue.put(ProcessStatus(process_id, result, consumed_time))
def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> str:
def _spawn_process(self, repository_id: RepositoryId, command: str, *args: str, **kwargs: str | None) -> str:
"""
spawn external ahriman process with supplied arguments
Args:
repository_id(RepositoryId): repository unique identifier
command(str): subcommand to run
*args(str): positional command arguments
**kwargs(str): named command arguments
Returns:
str: spawned process id
str: spawned process identifier
"""
# default arguments
arguments = self.command_arguments[:]
@ -125,12 +123,14 @@ class Spawn(Thread, LazyLogging):
arguments.append(value)
process_id = str(uuid.uuid4())
self.logger.info("full command line arguments of %s are %s", process_id, arguments)
self.logger.info("full command line arguments of %s are %s using repository %s",
process_id, arguments, repository_id)
parsed = self.args_parser.parse_args(arguments)
callback = parsed.handler.call
process = Process(target=self.process,
args=(callback, parsed, self.repository_id, process_id, self.queue),
args=(callback, parsed, repository_id, process_id, self.queue),
daemon=True)
process.start()
@ -160,66 +160,73 @@ class Spawn(Thread, LazyLogging):
server(str | None): PGP key server
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {} if server is None else {"key-server": server}
return self._spawn_process("service-key-import", key, **kwargs)
repository_id = RepositoryId("", "")
return self._spawn_process(repository_id, "service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> str:
def packages_add(self, repository_id: RepositoryId, packages: Iterable[str], username: str | None, *,
now: bool) -> str:
"""
add packages
Args:
repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
now(bool): build packages now
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {"username": username}
if now:
kwargs["now"] = ""
return self._spawn_process("package-add", *packages, **kwargs)
return self._spawn_process(repository_id, "package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str, username: str | None) -> str:
def packages_rebuild(self, repository_id: RepositoryId, depends_on: str, username: str | None) -> str:
"""
rebuild packages which depend on the specified package
Args:
repository_id(RepositoryId): repository unique identifier
depends_on(str): packages dependency
username(str | None): optional override of username for build process
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {"depends-on": depends_on, "username": username}
return self._spawn_process("repo-rebuild", **kwargs)
return self._spawn_process(repository_id, "repo-rebuild", **kwargs)
def packages_remove(self, packages: Iterable[str]) -> str:
def packages_remove(self, repository_id: RepositoryId, packages: Iterable[str]) -> str:
"""
remove packages
Args:
repository_id(RepositoryId): repository unique identifier
packages(Iterable[str]): packages list to remove
Returns:
str: spawned process id
str: spawned process identifier
"""
return self._spawn_process("package-remove", *packages)
return self._spawn_process(repository_id, "package-remove", *packages)
def packages_update(self, username: str | None, *, aur: bool, local: bool, manual: bool) -> str:
def packages_update(self, repository_id: RepositoryId, username: str | None, *,
aur: bool, local: bool, manual: bool) -> str:
"""
run full repository update
Args:
repository_id(RepositoryId): repository unique identifier
username(str | None): optional override of username for build process
aur(bool): check for aur updates
local(bool): check for local packages updates
manual(bool): check for manual packages
Returns:
str: spawned process id
str: spawned process identifier
"""
kwargs = {
"username": username,
@ -227,18 +234,18 @@ class Spawn(Thread, LazyLogging):
self.boolean_action_argument("local", local): "",
self.boolean_action_argument("manual", manual): "",
}
return self._spawn_process("repo-update", **kwargs)
return self._spawn_process(repository_id, "repo-update", **kwargs)
def run(self) -> None:
"""
thread run method
"""
for process_id, status, consumed_time in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s, consumed time %s",
process_id, status, consumed_time / 1000)
for terminated in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s, consumed time %ss",
terminated.process_id, terminated.status, terminated.consumed_time / 1000)
with self.lock:
process = self.active.pop(process_id, None)
process = self.active.pop(terminated.process_id, None)
if process is not None:
process.join()

View File

@ -26,6 +26,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
class Client:
@ -34,11 +35,12 @@ class Client:
"""
@staticmethod
def load(configuration: Configuration, *, report: bool) -> Client:
def load(repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Client:
"""
load client from settings
Args:
repository_id(RepositoryId): repository unqiue identifier
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
@ -58,7 +60,7 @@ class Client:
# but it will totally break used experience
if address or (host and port) or socket:
from ahriman.core.status.web_client import WebClient
return WebClient(configuration)
return WebClient(repository_id, configuration)
return Client()
def package_add(self, package: Package, status: BuildStatusEnum) -> None:

View File

@ -17,11 +17,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.log import LazyLogging
from ahriman.core.repository import Repository
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
@ -36,23 +34,20 @@ class Watcher(LazyLogging):
database(SQLite): database instance
known(dict[str, tuple[Package, BuildStatus]]): list of known packages. For the most cases ``packages`` should
be used instead
repository(Repository): repository object
repository_id(RepositoryId): repository unique identifier
status(BuildStatus): daemon status
"""
def __init__(self, repository_id: RepositoryId, configuration: Configuration, database: SQLite) -> None:
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
database(SQLite): database instance
"""
self.repository_id = repository_id
self.database = database
self.repository = Repository.load(repository_id, configuration, database, report=False)
self.known: dict[str, tuple[Package, BuildStatus]] = {}
self.status = BuildStatus()
@ -72,20 +67,12 @@ class Watcher(LazyLogging):
def load(self) -> None:
"""
load packages from local repository. In case if last status is known, it will use it
load packages from local database
"""
for package in self.repository.packages():
# get status of build or assign unknown
if (current := self.known.get(package.base)) is not None:
_, status = current
else:
status = BuildStatus()
self.known = {} # reset state
for package, status in self.database.packages_get(self.repository_id):
self.known[package.base] = (package, status)
for package, status in self.database.packages_get():
if package.base in self.known:
self.known[package.base] = (package, status)
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
"""
extract logs for the package base
@ -98,7 +85,7 @@ class Watcher(LazyLogging):
Returns:
list[tuple[float, str]]: package logs
"""
return self.database.logs_get(package_base, limit, offset)
return self.database.logs_get(package_base, limit, offset, self.repository_id)
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
@ -108,7 +95,7 @@ class Watcher(LazyLogging):
package_base(str): package base
version(str): package versio
"""
self.database.logs_remove(package_base, version)
self.database.logs_remove(package_base, version, self.repository_id)
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
@ -123,7 +110,7 @@ class Watcher(LazyLogging):
# there is new log record, so we remove old ones
self.logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record)
self.database.logs_insert(log_record_id, created, record, self.repository_id)
def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
"""
@ -151,7 +138,7 @@ class Watcher(LazyLogging):
package_base(str): package base
"""
self.known.pop(package_base, None)
self.database.package_remove(package_base)
self.database.package_remove(package_base, self.repository_id)
self.logs_remove(package_base, None)
def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
@ -173,7 +160,7 @@ class Watcher(LazyLogging):
raise UnknownPackageError(package_base) from None
full_status = BuildStatus(status)
self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status)
self.database.package_update(package, full_status, self.repository_id)
def status_update(self, status: BuildStatusEnum) -> None:
"""

View File

@ -32,6 +32,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
class WebClient(Client, SyncHttpClient):
@ -40,19 +41,22 @@ class WebClient(Client, SyncHttpClient):
Attributes:
address(str): address of the web service
repository_id(RepositoryId): repository unique identifier
use_unix_socket(bool): use websocket or not
"""
def __init__(self, configuration: Configuration) -> None:
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
"""
default constructor
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
"""
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
SyncHttpClient.__init__(self, configuration, "web", suppress_errors=suppress_errors)
self.repository_id = repository_id
self.address, self.use_unix_socket = self.parse_address(configuration)
@cached_property
@ -184,7 +188,8 @@ class WebClient(Client, SyncHttpClient):
"package": package.view()
}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package.base), json=payload)
self.make_request("POST", self._package_url(package.base),
params=self.repository_id.query(), json=payload)
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
@ -197,7 +202,8 @@ class WebClient(Client, SyncHttpClient):
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
with contextlib.suppress(Exception):
response = self.make_request("GET", self._package_url(package_base or ""))
response = self.make_request("GET", self._package_url(package_base or ""),
params=self.repository_id.query())
response_json = response.json()
return [
@ -224,7 +230,8 @@ class WebClient(Client, SyncHttpClient):
# this is special case, because we would like to do not suppress exception here
# in case of exception raised it will be handled by upstream HttpLogHandler
# In the other hand, we force to suppress all http logs here to avoid cyclic reporting
self.make_request("POST", self._logs_url(log_record_id.package_base), json=payload, suppress_errors=True)
self.make_request("POST", self._logs_url(log_record_id.package_base),
params=self.repository_id.query(), json=payload, suppress_errors=True)
def package_remove(self, package_base: str) -> None:
"""
@ -234,7 +241,7 @@ class WebClient(Client, SyncHttpClient):
package_base(str): basename to remove
"""
with contextlib.suppress(Exception):
self.make_request("DELETE", self._package_url(package_base))
self.make_request("DELETE", self._package_url(package_base), params=self.repository_id.query())
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
@ -246,7 +253,8 @@ class WebClient(Client, SyncHttpClient):
"""
payload = {"status": status.value}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package_base), json=payload)
self.make_request("POST", self._package_url(package_base),
params=self.repository_id.query(), json=payload)
def status_get(self) -> InternalStatus:
"""
@ -256,7 +264,7 @@ class WebClient(Client, SyncHttpClient):
InternalStatus: current internal (web) service status
"""
with contextlib.suppress(Exception):
response = self.make_request("GET", self._status_url())
response = self.make_request("GET", self._status_url(), params=self.repository_id.query())
response_json = response.json()
return InternalStatus.from_json(response_json)
@ -272,4 +280,4 @@ class WebClient(Client, SyncHttpClient):
"""
payload = {"status": status.value}
with contextlib.suppress(Exception):
self.make_request("POST", self._status_url(), json=payload)
self.make_request("POST", self._status_url(), params=self.repository_id.query(), json=payload)

View File

@ -51,7 +51,7 @@ class RemoteService(Upload, HttpUpload):
"""
Upload.__init__(self, repository_id, configuration)
HttpUpload.__init__(self, configuration, section)
self.client = WebClient(configuration)
self.client = WebClient(repository_id, configuration)
@cached_property
def session(self) -> requests.Session:
@ -81,7 +81,8 @@ class RemoteService(Upload, HttpUpload):
if signature_path is not None:
files["signature"] = signature_path.name, signature_path.open("rb"), "application/octet-stream", {}
self.make_request("POST", f"{self.client.address}/api/v1/service/upload", files=files)
self.make_request("POST", f"{self.client.address}/api/v1/service/upload",
params=self.repository_id.query(), files=files)
finally:
for _, fd, _, _ in files.values():
fd.close()

View File

@ -214,19 +214,18 @@ class Package(LazyLogging):
)
@classmethod
def from_aur(cls, name: str, pacman: Pacman, packager: str | None = None) -> Self:
def from_aur(cls, name: str, packager: str | None = None) -> Self:
"""
construct package properties from AUR page
Args:
name(str): package name (either base or normal name)
pacman(Pacman): alpm wrapper instance
packager(str | None, optional): packager to be used for this build (Default value = None)
Returns:
Self: package properties
"""
package = AUR.info(name, pacman=pacman)
package = AUR.info(name)
remote = RemoteSource(
source=PackageSource.AUR,
@ -331,7 +330,7 @@ class Package(LazyLogging):
Returns:
Self: package properties
"""
package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name, pacman=pacman)
package = OfficialSyncdb.info(name, pacman=pacman) if use_syncdb else Official.info(name)
remote = RemoteSource(
source=PackageSource.Repository,

View File

@ -0,0 +1,36 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from dataclasses import dataclass
@dataclass(frozen=True)
class ProcessStatus:
"""
terminated process status descriptor
Attributes:
process_id(str): unique process identifier
status(bool): process exit code status
consumed_time(int): consumed time in ms
"""
process_id: str
status: bool
consumed_time: int

View File

@ -54,6 +54,27 @@ class RepositoryId:
"""
return f"{self.architecture}-{self.name}" # basically the same as used for command line
def query(self) -> list[tuple[str, str]]:
"""
generate query parameters
Returns:
list[tuple[str, str]]: json view as query parameters
"""
return list(self.view().items())
def view(self) -> dict[str, Any]:
"""
generate json package view
Returns:
dict[str, Any]: json-friendly dictionary
"""
return {
"architecture": self.architecture,
"repository": self.name,
}
def __lt__(self, other: Any) -> bool:
"""
comparison operator for sorting

View File

@ -38,6 +38,7 @@ from ahriman.web.schemas.pgp_key_schema import PGPKeySchema
from ahriman.web.schemas.process_id_schema import ProcessIdSchema
from ahriman.web.schemas.process_schema import ProcessSchema
from ahriman.web.schemas.remote_schema import RemoteSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.search_schema import SearchSchema
from ahriman.web.schemas.status_schema import StatusSchema
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema

View File

@ -17,29 +17,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from marshmallow import fields
from ahriman import __version__
from ahriman.web.schemas.counters_schema import CountersSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.status_schema import StatusSchema
class InternalStatusSchema(Schema):
class InternalStatusSchema(RepositoryIdSchema):
"""
response service status schema
"""
architecture = fields.String(required=True, metadata={
"description": "Repository architecture",
"example": "x86_64",
})
packages = fields.Nested(CountersSchema(), required=True, metadata={
"description": "Repository package counters",
})
repository = fields.String(required=True, metadata={
"description": "Repository name",
"example": "repo-clone",
})
status = fields.Nested(StatusSchema(), required=True, metadata={
"description": "Repository status as stored by web service",
})

View File

@ -17,12 +17,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from marshmallow import fields
from ahriman import __version__
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class LogSchema(Schema):
class LogSchema(RepositoryIdSchema):
"""
request package log schema
"""

View File

@ -21,6 +21,7 @@ from marshmallow import Schema, fields
from ahriman.models.build_status import BuildStatusEnum
from ahriman.web.schemas.package_schema import PackageSchema
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
from ahriman.web.schemas.status_schema import StatusSchema
@ -35,6 +36,9 @@ class PackageStatusSimplifiedSchema(Schema):
status = fields.Enum(BuildStatusEnum, by_value=True, required=True, metadata={
"description": "Current status",
})
repository = fields.Nested(RepositoryIdSchema(), required=True, metadata={
"description": "Repository identifier",
})
class PackageStatusSchema(Schema):
@ -48,3 +52,6 @@ class PackageStatusSchema(Schema):
status = fields.Nested(StatusSchema(), required=True, metadata={
"description": "Last package status",
})
repository = fields.Nested(RepositoryIdSchema(), required=True, metadata={
"description": "Repository identifier",
})

View File

@ -17,10 +17,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
from marshmallow import fields
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
class PaginationSchema(Schema):
class PaginationSchema(RepositoryIdSchema):
"""
request pagination schema
"""

View File

@ -0,0 +1,35 @@
#
# Copyright (c) 2021-2023 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from marshmallow import Schema, fields
class RepositoryIdSchema(Schema):
"""
request and response repository unique identifier schema
"""
architecture = fields.String(metadata={
"description": "Repository architecture",
"example": "x86_64",
})
repository = fields.String(metadata={
"description": "Repository name",
"example": "aur-clone",
})

View File

@ -24,8 +24,10 @@ from typing import Any, TypeVar
from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
from ahriman.core.spawn import Spawn
from ahriman.core.status.watcher import Watcher
from ahriman.models.repository_id import RepositoryId
from ahriman.models.user_access import UserAccess
@ -56,15 +58,25 @@ class BaseView(View, CorsViewMixin):
return configuration
@property
def service(self) -> Watcher:
def services(self) -> dict[RepositoryId, Watcher]:
"""
get status watcher instance
get all loaded watchers
Returns:
Watcher: build status watcher instance
dict[RepositoryId, Watcher]: map of loaded watchers per known repository
"""
watcher: Watcher = self.request.app["watcher"]
return watcher
watchers: dict[RepositoryId, Watcher] = self.request.app["watcher"]
return watchers
@property
def sign(self) -> GPG:
"""
get GPG control instance
Returns:
GPG: GPG wrapper instance
"""
return GPG(self.configuration)
@property
def spawner(self) -> Spawn:
@ -197,8 +209,8 @@ class BaseView(View, CorsViewMixin):
HTTPBadRequest: if supplied parameters are invalid
"""
try:
limit = int(self.request.query.getone("limit", default=-1))
offset = int(self.request.query.getone("offset", default=0))
limit = int(self.request.query.get("limit", default=-1))
offset = int(self.request.query.get("offset", default=0))
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
@ -210,6 +222,34 @@ class BaseView(View, CorsViewMixin):
return limit, offset
def repository_id(self) -> RepositoryId:
"""
extract repository from request
Returns:
RepositoryIde: repository if possible to construct and first one otherwise
"""
architecture = self.request.query.get("architecture")
name = self.request.query.get("repository")
if architecture and name:
return RepositoryId(architecture, name)
return next(iter(sorted(self.services.keys())))
def service(self, repository_id: RepositoryId | None = None) -> Watcher:
"""
get status watcher instance
Args:
repository_id(RepositoryId | None, optional): repository unique identifier (Default value = None)
Returns:
Watcher: build status watcher instance. If no repository provided, it will return the first one
"""
if repository_id is None:
repository_id = self.repository_id()
return self.services[repository_id]
async def username(self) -> str | None:
"""
extract username from request if any

View File

@ -37,7 +37,10 @@ class IndexView(BaseView):
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
* index_url - url to the repository index, string, optional
* repository - repository name, string, required
* repositories - list of repositories unique identifiers, required
* id - unique repository identifier, string, required
* repository - repository name, string, required
* architecture - repository architecture, string, required
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
@ -64,5 +67,11 @@ class IndexView(BaseView):
return {
"auth": auth,
"index_url": self.configuration.get("web", "index_url", fallback=None),
"repository": self.service.repository.name,
"repositories": [
{
"id": repository.id,
**repository.view(),
}
for repository in sorted(self.services)
]
}

View File

@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -51,6 +51,7 @@ class AddView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@ -68,7 +69,8 @@ class AddView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_add(packages, username, now=True)
process_id = self.spawner.packages_add(repository_id, packages, username, now=True)
return json_response({"process_id": process_id})

View File

@ -67,13 +67,13 @@ class PGPView(BaseView):
HTTPNotFound: if key wasn't found or service was unable to fetch it
"""
try:
key = self.get_non_empty(self.request.query.getone, "key")
server = self.get_non_empty(self.request.query.getone, "server")
key = self.get_non_empty(self.request.query.get, "key")
server = self.get_non_empty(self.request.query.get, "server")
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
try:
key = self.service.repository.sign.key_download(server, key)
key = self.sign.key_download(server, key)
except Exception:
raise HTTPNotFound

View File

@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -51,6 +51,7 @@ class RebuildView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@ -69,7 +70,8 @@ class RebuildView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_rebuild(depends_on, username)
process_id = self.spawner.packages_rebuild(repository_id, depends_on, username)
return json_response({"process_id": process_id})

View File

@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -51,6 +51,7 @@ class RemoveView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@ -68,6 +69,7 @@ class RemoveView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
process_id = self.spawner.packages_remove(packages)
repository_id = self.repository_id()
process_id = self.spawner.packages_remove(repository_id, packages)
return json_response({"process_id": process_id})

View File

@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNamesSchema, ProcessIdSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -51,6 +51,7 @@ class RequestView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageNamesSchema)
async def post(self) -> Response:
"""
@ -69,6 +70,7 @@ class RequestView(BaseView):
raise HTTPBadRequest(reason=str(ex))
username = await self.username()
process_id = self.spawner.packages_add(packages, username, now=False)
repository_id = self.repository_id()
process_id = self.spawner.packages_add(repository_id, packages, username, now=False)
return json_response({"process_id": process_id})

View File

@ -69,7 +69,7 @@ class SearchView(BaseView):
"""
try:
search: list[str] = self.get_non_empty(lambda key: self.request.query.getall(key, default=[]), "for")
packages = AUR.multisearch(*search, pacman=self.service.repository.pacman)
packages = AUR.multisearch(*search)
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))

View File

@ -22,7 +22,7 @@ import aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import HTTPBadRequest, Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, UpdateFlagsSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, ProcessIdSchema, UpdateFlagsSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -51,6 +51,7 @@ class UpdateView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(UpdateFlagsSchema)
async def post(self) -> Response:
"""
@ -67,8 +68,10 @@ class UpdateView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
repository_id = self.repository_id()
username = await self.username()
process_id = self.spawner.packages_update(
repository_id,
username,
aur=data.get("aur", True),
local=data.get("local", True),

View File

@ -25,8 +25,9 @@ from aiohttp.web import HTTPBadRequest, HTTPCreated, HTTPNotFound
from pathlib import Path
from tempfile import NamedTemporaryFile
from ahriman.models.repository_paths import RepositoryPaths
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, FileSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -106,6 +107,7 @@ class UploadView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.form_schema(FileSchema)
async def post(self) -> None:
"""
@ -125,7 +127,10 @@ class UploadView(BaseView):
raise HTTPBadRequest(reason=str(ex))
max_body_size = self.configuration.getint("web", "max_body_size", fallback=None)
target = self.configuration.repository_paths.packages
paths_root = self.configuration.repository_paths.root
repository_id = self.repository_id()
target = RepositoryPaths(paths_root, repository_id).packages
files = []
while (part := await reader.next()) is not None:

View File

@ -25,7 +25,7 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.util import pretty_datetime
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -57,6 +57,7 @@ class LogsView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def delete(self) -> None:
"""
delete package logs
@ -65,7 +66,7 @@ class LogsView(BaseView):
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service.logs_remove(package_base, None)
self.service().logs_remove(package_base, None)
raise HTTPNoContent
@ -84,6 +85,7 @@ class LogsView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def get(self) -> Response:
"""
get last package logs
@ -97,10 +99,10 @@ class LogsView(BaseView):
package_base = self.request.match_info["package"]
try:
_, status = self.service.package_get(package_base)
_, status = self.service().package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound
logs = self.service.logs_get(package_base)
logs = self.service().logs_get(package_base)
response = {
"package_base": package_base,
@ -143,6 +145,6 @@ class LogsView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service.logs_update(LogRecordId(package_base, version), created, record)
self.service().logs_update(LogRecordId(package_base, version), created, record)
raise HTTPNoContent

View File

@ -25,7 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, PackageStatusSimplifiedSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PackageStatusSchema, \
PackageStatusSimplifiedSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -57,6 +58,7 @@ class PackageView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def delete(self) -> None:
"""
delete package base from status page
@ -65,7 +67,7 @@ class PackageView(BaseView):
HTTPNoContent: on success response
"""
package_base = self.request.match_info["package"]
self.service.package_remove(package_base)
self.service().package_remove(package_base)
raise HTTPNoContent
@ -84,6 +86,7 @@ class PackageView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def get(self) -> Response:
"""
get current package base status
@ -95,16 +98,18 @@ class PackageView(BaseView):
HTTPNotFound: if no package was found
"""
package_base = self.request.match_info["package"]
repository_id = self.repository_id()
try:
package, status = self.service.package_get(package_base)
package, status = self.service(repository_id).package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound
response = [
{
"package": package.view(),
"status": status.view()
"status": status.view(),
"repository": repository_id.view(),
}
]
return json_response(response)
@ -124,6 +129,7 @@ class PackageView(BaseView):
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.match_info_schema(PackageNameSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(PackageStatusSimplifiedSchema)
async def post(self) -> None:
"""
@ -143,7 +149,7 @@ class PackageView(BaseView):
raise HTTPBadRequest(reason=str(ex))
try:
self.service.package_update(package_base, status, package)
self.service().package_update(package_base, status, package)
except UnknownPackageError:
raise HTTPBadRequest(reason=f"Package {package_base} is unknown, but no package body set")

View File

@ -26,7 +26,7 @@ from aiohttp.web import HTTPNoContent, Response, json_response
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageStatusSchema, PaginationSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -68,12 +68,16 @@ class PackagesView(BaseView):
limit, offset = self.page()
stop = offset + limit if limit >= 0 else None
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda pair: pair[0].base
repository_id = self.repository_id()
packages = self.service(repository_id).packages
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda items: items[0].base
response = [
{
"package": package.view(),
"status": status.view()
} for package, status in itertools.islice(sorted(self.service.packages, key=comparator), offset, stop)
"status": status.view(),
"repository": repository_id.view(),
} for package, status in itertools.islice(sorted(packages, key=comparator), offset, stop)
]
return json_response(response)
@ -91,6 +95,7 @@ class PackagesView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
async def post(self) -> None:
"""
reload all packages from repository
@ -98,6 +103,6 @@ class PackagesView(BaseView):
Raises:
HTTPNoContent: on success response
"""
self.service.load()
self.service().load()
raise HTTPNoContent

View File

@ -0,0 +1,65 @@
#
# Copyright (c) 2021-2023 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 aiohttp_apispec # type: ignore[import-untyped]
from aiohttp.web import Response, json_response
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
class RepositoriesView(BaseView):
"""
repositories view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION = UserAccess.Read
ROUTES = ["/api/v1/repositories"]
@aiohttp_apispec.docs(
tags=["Status"],
summary="Available repositories",
description="List available repositories",
responses={
200: {"description": "Success response", "schema": RepositoryIdSchema(many=True)},
401: {"description": "Authorization required", "schema": ErrorSchema},
403: {"description": "Access is forbidden", "schema": ErrorSchema},
500: {"description": "Internal server error", "schema": ErrorSchema},
},
security=[{"token": [GET_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
async def get(self) -> Response:
"""
get list of available repositories
Returns:
Response: 200 with service status object
"""
repositories = [
repository_id.view()
for repository_id in sorted(self.services)
]
return json_response(repositories)

View File

@ -26,7 +26,7 @@ from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.counters import Counters
from ahriman.models.internal_status import InternalStatus
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema
from ahriman.web.schemas import AuthSchema, ErrorSchema, InternalStatusSchema, StatusSchema, RepositoryIdSchema
from ahriman.web.views.base import BaseView
@ -63,12 +63,13 @@ class StatusView(BaseView):
Returns:
Response: 200 with service status object
"""
counters = Counters.from_packages(self.service.packages)
repository_id = self.repository_id()
counters = Counters.from_packages(self.service(repository_id).packages)
status = InternalStatus(
status=self.service.status,
architecture=self.service.repository_id.architecture,
status=self.service(repository_id).status,
architecture=repository_id.architecture,
packages=counters,
repository=self.service.repository_id.name,
repository=repository_id.name,
version=__version__,
)
@ -88,6 +89,7 @@ class StatusView(BaseView):
security=[{"token": [POST_PERMISSION]}],
)
@aiohttp_apispec.cookies_schema(AuthSchema)
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
@aiohttp_apispec.json_schema(StatusSchema)
async def post(self) -> None:
"""
@ -103,6 +105,6 @@ class StatusView(BaseView):
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service.status_update(status)
self.service().status_update(status)
raise HTTPNoContent

View File

@ -75,7 +75,7 @@ class LoginView(BaseView):
if not isinstance(oauth_provider, OAuth): # there is actually property, but mypy does not like it anyway
raise HTTPMethodNotAllowed(self.request.method, ["POST"])
code = self.request.query.getone("code", default=None)
code = self.request.query.get("code")
if not code:
raise HTTPFound(oauth_provider.get_oauth_url())

View File

@ -23,8 +23,7 @@ from aiohttp.web import HTTPNotFound, Response, json_response
from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.user_access import UserAccess
from ahriman.web.schemas import AuthSchema, ErrorSchema, PackageNameSchema, PaginationSchema
from ahriman.web.schemas.logs_schema import LogsSchemaV2
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchemaV2, PackageNameSchema, PaginationSchema
from ahriman.web.views.base import BaseView
@ -70,10 +69,10 @@ class LogsView(BaseView):
limit, offset = self.page()
try:
_, status = self.service.package_get(package_base)
_, status = self.service().package_get(package_base)
except UnknownPackageError:
raise HTTPNotFound
logs = self.service.logs_get(package_base, limit, offset)
logs = self.service().logs_get(package_base, limit, offset)
response = {
"package_base": package_base,

View File

@ -38,7 +38,7 @@ from ahriman.web.middlewares.exception_handler import exception_handler
from ahriman.web.routes import setup_routes
__all__ = ["run_server", "setup_service"]
__all__ = ["run_server", "setup_server"]
def _create_socket(configuration: Configuration, application: Application) -> socket.socket | None:
@ -95,8 +95,10 @@ async def _on_startup(application: Application) -> None:
InitializeError: in case if matched could not be loaded
"""
application.logger.info("server started")
try:
application["watcher"].load()
for watcher in application["watcher"].values():
watcher.load()
except Exception:
message = "could not load packages"
application.logger.exception(message)
@ -121,17 +123,20 @@ def run_server(application: Application) -> None:
access_log=logging.getLogger("http"), access_log_class=FilteredAccessLogger)
def setup_service(repository_id: RepositoryId, configuration: Configuration, spawner: Spawn) -> Application:
def setup_server(configuration: Configuration, spawner: Spawn, repositories: list[RepositoryId]) -> Application:
"""
create web application
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
spawner(Spawn): spawner thread
repositories(list[RepositoryId]): list of known repositories
Returns:
Application: web application instance
Raises:
InitializeError: if no repositories set
"""
application = Application(logger=logging.getLogger(__name__))
application.on_shutdown.append(_on_shutdown)
@ -153,11 +158,15 @@ def setup_service(repository_id: RepositoryId, configuration: Configuration, spa
application.logger.info("setup configuration")
application["configuration"] = configuration
application.logger.info("setup database and perform migrations")
database = application["database"] = SQLite.load(configuration)
application.logger.info("setup watcher")
application["watcher"] = Watcher(repository_id, configuration, database)
application.logger.info("setup watchers")
if not repositories:
raise InitializeError("No repositories configured, exiting")
database = SQLite.load(configuration)
watchers: dict[RepositoryId, Watcher] = {}
for repository_id in repositories:
application.logger.info("load repository %s", repository_id)
watchers[repository_id] = Watcher(repository_id, database)
application["watcher"] = watchers
application.logger.info("setup process spawner")
application["spawn"] = spawner

View File

@ -99,8 +99,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, application.repository.pacman, package_ahriman.packager),
MockCall("python-installer", application.repository.pacman, package_ahriman.packager),
MockCall(package_python_schedule.base, package_ahriman.packager),
MockCall("python-installer", package_ahriman.packager),
], any_order=True)
package_local_mock.assert_has_calls([
MockCall(application.repository.paths.cache_for("python"), "x86_64", package_ahriman.packager),

View File

@ -30,7 +30,12 @@ def test_call(args: argparse.Namespace, configuration: Configuration, mocker: Mo
assert Handler.call(args, repository_id)
configuration_mock.assert_called_once_with(args.configuration, repository_id)
log_handler_mock.assert_called_once_with(args.log_handler)
log_load_mock.assert_called_once_with(configuration, args.log_handler, quiet=args.quiet, report=args.report)
log_load_mock.assert_called_once_with(
repository_id,
configuration,
args.log_handler,
quiet=args.quiet,
report=args.report)
enter_mock.assert_called_once_with()
exit_mock.assert_called_once_with(None, None, None)
@ -115,13 +120,24 @@ def test_run(args: argparse.Namespace, configuration: Configuration) -> None:
Handler.run(args, repository_id, configuration, report=True)
def test_check_if_empty() -> None:
"""
must raise exception in case if predicate is True and enabled
"""
Handler.check_if_empty(False, False)
Handler.check_if_empty(True, False)
Handler.check_if_empty(False, True)
with pytest.raises(ExitCode):
Handler.check_if_empty(True, True)
def test_repositories_extract(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must generate list of available repositories based on flags
"""
args.architecture = ["arch"]
args.architecture = "arch"
args.configuration = configuration.path
args.repository = ["repo"]
args.repository = "repo"
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
@ -135,7 +151,7 @@ def test_repositories_extract_repository(args: argparse.Namespace, configuration
"""
must generate list of available repositories based on flags and tree
"""
args.architecture = ["arch"]
args.architecture = "arch"
args.configuration = configuration.path
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
@ -151,7 +167,7 @@ def test_repositories_extract_repository_legacy(args: argparse.Namespace, config
"""
must generate list of available repositories based on flags and tree
"""
args.architecture = ["arch"]
args.architecture = "arch"
args.configuration = configuration.path
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
@ -168,7 +184,7 @@ def test_repositories_extract_architecture(args: argparse.Namespace, configurati
must read repository name from config
"""
args.configuration = configuration.path
args.repository = ["repo"]
args.repository = "repo"
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 +223,21 @@ def test_repositories_extract_systemd(args: argparse.Namespace, configuration: C
known_repositories_mock.assert_not_called()
def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
must extract repository list by using dash separated identifier
"""
args.configuration = configuration.path
args.repository_id = "i686-some-repo-name"
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
assert Handler.repositories_extract(args) == [RepositoryId("i686", "some-repo-name")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_not_called()
def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configuration: Configuration,
mocker: MockerFixture) -> None:
"""
@ -221,14 +252,3 @@ def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configura
assert Handler.repositories_extract(args) == [RepositoryId("i686", "aur-clone")]
known_architectures_mock.assert_not_called()
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
def test_check_if_empty() -> None:
"""
must raise exception in case if predicate is True and enabled
"""
Handler.check_if_empty(False, False)
Handler.check_if_empty(True, False)
Handler.check_if_empty(False, True)
with pytest.raises(ExitCode):
Handler.check_if_empty(True, True)

View File

@ -1,4 +1,5 @@
import argparse
import pytest
from pytest_mock import MockerFixture
@ -16,6 +17,9 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.info = False
args.key = None
args.section = None
args.secure = True
return args
@ -35,6 +39,48 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
print_mock.assert_called()
def test_run_info(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with info
"""
args = _default_args(args)
args.info = True
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
_, repository_id = configuration.check_loaded()
Dump.run(args, repository_id, configuration, report=False)
print_mock.assert_called()
def test_run_section(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with filter by section
"""
args = _default_args(args)
args.section = "settings"
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
_, repository_id = configuration.check_loaded()
Dump.run(args, repository_id, configuration, report=False)
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=" = ")
def test_run_section_key(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command with filter by section and key
"""
args = _default_args(args)
args.section = "settings"
args.key = "include"
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
application_mock = mocker.patch("ahriman.core.configuration.Configuration.dump")
_, repository_id = configuration.check_loaded()
Dump.run(args, repository_id, configuration, report=False)
application_mock.assert_not_called()
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run

View File

@ -0,0 +1,37 @@
import argparse
import pytest
from pytest_mock import MockerFixture
from ahriman.application.handlers import Repositories
from ahriman.core.configuration import Configuration
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
"""
default arguments for these test cases
Args:
args(argparse.Namespace): command line arguments fixture
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.configuration = None # doesn't matter actually
args.id_only = False
return args
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
must run command
"""
args = _default_args(args)
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
_, repository_id = configuration.check_loaded()
application_mock = mocker.patch("ahriman.application.handlers.Handler.repositories_extract",
return_value=[repository_id])
Repositories.run(args, repository_id, configuration, report=False)
application_mock.assert_called_once_with(pytest.helpers.anyvar(int))
print_mock.assert_called_once_with(verbose=not args.id_only, log_fn=pytest.helpers.anyvar(int), separator=": ")

View File

@ -44,8 +44,8 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
_, repository_id = configuration.check_loaded()
Search.run(args, repository_id, configuration, report=False)
aur_search_mock.assert_called_once_with("ahriman", pacman=pytest.helpers.anyvar(int))
official_search_mock.assert_called_once_with("ahriman", pacman=pytest.helpers.anyvar(int))
aur_search_mock.assert_called_once_with("ahriman")
official_search_mock.assert_called_once_with("ahriman")
check_mock.assert_called_once_with(False, False)
print_mock.assert_has_calls([
MockCall(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": "),

View File

@ -38,7 +38,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
_, repository_id = configuration.check_loaded()
ServiceUpdates.run(args, repository_id, configuration, report=False)
package_mock.assert_called_once_with(package_ahriman.base, repository.pacman, None)
package_mock.assert_called_once_with(package_ahriman.base, None)
application_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator=" -> ")
check_mock.assert_called_once_with(args.exit_code, True)

View File

@ -25,7 +25,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
Returns:
argparse.Namespace: generated arguments for these test cases
"""
args.architecture = ["x86_64"]
args.architecture = "x86_64"
args.build_as_user = "ahriman"
args.from_configuration = Path("/usr/share/devtools/pacman.conf.d/extra.conf")
args.generate_salt = True
@ -33,7 +33,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
args.mirror = "mirror"
args.multilib = True
args.packager = "John Doe <john@doe.com>"
args.repository = ["aur-clone"]
args.repository = "aur-clone"
args.server = None
args.sign_key = "key"
args.sign_target = [SignSettings.Packages]
@ -127,6 +127,7 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
mocker.patch("pathlib.Path.open")
set_option_mock = mocker.patch("ahriman.core.configuration.Configuration.set_option")
write_mock = mocker.patch("ahriman.core.configuration.Configuration.write")
remove_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
_, repository_id = configuration.check_loaded()
command = Setup.build_command(repository_paths.root, repository_id)
@ -143,13 +144,12 @@ def test_configuration_create_ahriman(args: argparse.Namespace, configuration: C
" ".join([target.name.lower() for target in args.sign_target])),
MockCall(Configuration.section_name("sign", repository_id.name, repository_id.architecture), "key",
args.sign_key),
MockCall(Configuration.section_name("web", repository_id.name, repository_id.architecture), "port",
str(args.web_port)),
MockCall(Configuration.section_name("web", repository_id.name, repository_id.architecture), "unix_socket",
str(args.web_unix_socket)),
MockCall("web", "port", str(args.web_port)),
MockCall("web", "unix_socket", str(args.web_unix_socket)),
MockCall("auth", "salt", pytest.helpers.anyvar(str, strict=True)),
])
write_mock.assert_called_once_with(pytest.helpers.anyvar(int))
remove_mock.assert_called_once_with(configuration.include / "00-setup-overrides.ini", missing_ok=True)
def test_configuration_create_ahriman_no_multilib(args: argparse.Namespace, configuration: Configuration,

View File

@ -20,6 +20,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
argparse.Namespace: generated arguments for these test cases
"""
args.parser = lambda: True
args.configuration = None # doesn't matter actually
args.force = False
args.log_handler = None
args.report = True
@ -35,15 +36,16 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
"""
args = _default_args(args)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
setup_mock = mocker.patch("ahriman.web.web.setup_service")
setup_mock = mocker.patch("ahriman.web.web.setup_server")
run_mock = mocker.patch("ahriman.web.web.run_server")
start_mock = mocker.patch("ahriman.core.spawn.Spawn.start")
stop_mock = mocker.patch("ahriman.core.spawn.Spawn.stop")
join_mock = mocker.patch("ahriman.core.spawn.Spawn.join")
_, repository_id = configuration.check_loaded()
mocker.patch("ahriman.application.handlers.Handler.repositories_extract", return_value=[repository_id])
Web.run(args, repository_id, configuration, report=False)
setup_mock.assert_called_once_with(repository_id, configuration, pytest.helpers.anyvar(int))
setup_mock.assert_called_once_with(configuration, pytest.helpers.anyvar(int), [repository_id])
run_mock.assert_called_once_with(pytest.helpers.anyvar(int))
start_mock.assert_called_once_with()
stop_mock.assert_called_once_with()
@ -54,35 +56,32 @@ def test_extract_arguments(args: argparse.Namespace, configuration: Configuratio
"""
must extract correct args
"""
_, repository_id = configuration.check_loaded()
expected = [
"--architecture", repository_id.architecture,
"--repository", repository_id.name,
"--configuration", str(configuration.path),
]
probe = _default_args(args)
assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected
assert list(Web.extract_arguments(probe, configuration)) == expected
probe.force = True
expected.extend(["--force"])
assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected
assert list(Web.extract_arguments(probe, configuration)) == expected
probe.log_handler = LogHandler.Console
expected.extend(["--log-handler", probe.log_handler.value])
assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected
assert list(Web.extract_arguments(probe, configuration)) == expected
probe.quiet = True
expected.extend(["--quiet"])
assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected
assert list(Web.extract_arguments(probe, configuration)) == expected
probe.unsafe = True
expected.extend(["--unsafe"])
assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected
assert list(Web.extract_arguments(probe, configuration)) == expected
configuration.set_option("web", "wait_timeout", "60")
expected.extend(["--wait-timeout", "60"])
assert list(Web.extract_arguments(probe, repository_id, configuration)) == expected
assert list(Web.extract_arguments(probe, configuration)) == expected
def test_extract_arguments_full(parser: argparse.ArgumentParser, configuration: Configuration):
@ -104,10 +103,7 @@ def test_extract_arguments_full(parser: argparse.ArgumentParser, configuration:
value = action.type(value)
setattr(args, action.dest, value)
_, repository_id = configuration.check_loaded()
assert list(Web.extract_arguments(args, repository_id, configuration)) == [
"--architecture", repository_id.architecture,
"--repository", repository_id.name,
assert list(Web.extract_arguments(args, configuration)) == [
"--configuration", str(configuration.path),
"--force",
"--log-handler", "console",

View File

@ -67,14 +67,6 @@ def test_parser_option_architecture_empty(parser: argparse.ArgumentParser) -> No
assert args.architecture is None
def test_parser_option_architecture_multiple(parser: argparse.ArgumentParser) -> None:
"""
must accept multiple architectures
"""
args = parser.parse_args(["-a", "x86_64", "-a", "i686", "service-config"])
assert args.architecture == ["x86_64", "i686"]
def test_parser_option_repository_empty(parser: argparse.ArgumentParser) -> None:
"""
must parse empty repository list as None
@ -83,24 +75,16 @@ def test_parser_option_repository_empty(parser: argparse.ArgumentParser) -> None
assert args.repository is None
def test_parser_option_repository_multiple(parser: argparse.ArgumentParser) -> None:
"""
must accept multiple architectures
"""
args = parser.parse_args(["-r", "repo1", "-r", "repo2", "service-config"])
assert args.repository == ["repo1", "repo2"]
def test_subparsers_aur_search(parser: argparse.ArgumentParser) -> None:
"""
aur-search command must imply architecture list, lock, quiet, report, repository and unsafe
"""
args = parser.parse_args(["aur-search", "ahriman"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -109,7 +93,7 @@ def test_subparsers_aur_search_option_architecture(parser: argparse.ArgumentPars
aur-search command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "aur-search", "ahriman"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_aur_search_option_repository(parser: argparse.ArgumentParser) -> None:
@ -117,7 +101,7 @@ def test_subparsers_aur_search_option_repository(parser: argparse.ArgumentParser
aur-search command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "aur-search", "ahriman"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_help(parser: argparse.ArgumentParser) -> None:
@ -125,11 +109,11 @@ def test_subparsers_help(parser: argparse.ArgumentParser) -> None:
help command must imply architecture list, lock, quiet, report, repository, unsafe and parser
"""
args = parser.parse_args(["help"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
assert args.parser is not None and args.parser()
@ -139,7 +123,7 @@ def test_subparsers_help_option_architecture(parser: argparse.ArgumentParser) ->
help command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "help"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_help_option_repository(parser: argparse.ArgumentParser) -> None:
@ -147,7 +131,7 @@ def test_subparsers_help_option_repository(parser: argparse.ArgumentParser) -> N
help command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "help"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_help_commands_unsafe(parser: argparse.ArgumentParser) -> None:
@ -155,11 +139,11 @@ def test_subparsers_help_commands_unsafe(parser: argparse.ArgumentParser) -> Non
help-commands-unsafe command must imply architecture list, lock, quiet, report, repository, unsafe and parser
"""
args = parser.parse_args(["help-commands-unsafe"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
assert args.parser is not None and args.parser()
@ -169,7 +153,7 @@ def test_subparsers_help_commands_unsafe_option_architecture(parser: argparse.Ar
help-commands-unsafe command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "help-commands-unsafe"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_help_commands_unsafe_option_repository(parser: argparse.ArgumentParser) -> None:
@ -177,7 +161,7 @@ def test_subparsers_help_commands_unsafe_option_repository(parser: argparse.Argu
help-commands-unsafe command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "help-commands-unsafe"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_help_updates(parser: argparse.ArgumentParser) -> None:
@ -185,11 +169,11 @@ def test_subparsers_help_updates(parser: argparse.ArgumentParser) -> None:
help-updates command must imply architecture list, lock, quiet, report, repository, and unsafe
"""
args = parser.parse_args(["help-updates"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -198,7 +182,7 @@ def test_subparsers_help_updates_option_architecture(parser: argparse.ArgumentPa
help-updates command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "help-updates"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_help_updates_option_repository(parser: argparse.ArgumentParser) -> None:
@ -206,7 +190,7 @@ def test_subparsers_help_updates_option_repository(parser: argparse.ArgumentPars
help-updates command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "help-updates"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_help_version(parser: argparse.ArgumentParser) -> None:
@ -214,11 +198,11 @@ def test_subparsers_help_version(parser: argparse.ArgumentParser) -> None:
help-version command must imply architecture, lock, quiet, report, repository and unsafe
"""
args = parser.parse_args(["help-version"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -227,7 +211,7 @@ def test_subparsers_help_version_option_architecture(parser: argparse.ArgumentPa
help-version command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "help-version"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_help_version_option_repository(parser: argparse.ArgumentParser) -> None:
@ -235,7 +219,7 @@ def test_subparsers_help_version_option_repository(parser: argparse.ArgumentPars
help-version command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "help-version"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_package_add_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -245,7 +229,7 @@ def test_subparsers_package_add_option_architecture(parser: argparse.ArgumentPar
args = parser.parse_args(["package-add", "ahriman"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "package-add", "ahriman"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_package_add_option_repository(parser: argparse.ArgumentParser) -> None:
@ -255,7 +239,7 @@ def test_subparsers_package_add_option_repository(parser: argparse.ArgumentParse
args = parser.parse_args(["package-add", "ahriman"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "package-add", "ahriman"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_package_add_option_refresh(parser: argparse.ArgumentParser) -> None:
@ -277,7 +261,7 @@ def test_subparsers_package_remove_option_architecture(parser: argparse.Argument
args = parser.parse_args(["package-remove", "ahriman"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "package-remove", "ahriman"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_package_remove_option_repository(parser: argparse.ArgumentParser) -> None:
@ -287,7 +271,7 @@ def test_subparsers_package_remove_option_repository(parser: argparse.ArgumentPa
args = parser.parse_args(["package-remove", "ahriman"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "package-remove", "ahriman"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_package_status(parser: argparse.ArgumentParser) -> None:
@ -295,11 +279,11 @@ def test_subparsers_package_status(parser: argparse.ArgumentParser) -> None:
package-status command must imply lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert args.unsafe
@ -308,12 +292,12 @@ def test_subparsers_package_status_remove(parser: argparse.ArgumentParser) -> No
package-status-remove command must imply action, lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-remove", "ahriman"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.action == Action.Remove
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert args.unsafe
@ -322,12 +306,12 @@ def test_subparsers_package_status_update(parser: argparse.ArgumentParser) -> No
package-status-update command must imply action, lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.action == Action.Update
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert args.unsafe
@ -347,10 +331,10 @@ def test_subparsers_patch_add(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["patch-add", "ahriman", "version"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_patch_add_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -358,7 +342,7 @@ def test_subparsers_patch_add_option_architecture(parser: argparse.ArgumentParse
patch-add command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-add", "ahriman", "version"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_patch_add_option_repository(parser: argparse.ArgumentParser) -> None:
@ -366,7 +350,7 @@ def test_subparsers_patch_add_option_repository(parser: argparse.ArgumentParser)
patch-add command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "patch-add", "ahriman", "version"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None:
@ -375,10 +359,10 @@ def test_subparsers_patch_list(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["patch-list", "ahriman"])
assert args.action == Action.List
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -387,7 +371,7 @@ def test_subparsers_patch_list_option_architecture(parser: argparse.ArgumentPars
patch-list command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-list", "ahriman"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_patch_list_option_repository(parser: argparse.ArgumentParser) -> None:
@ -395,7 +379,7 @@ def test_subparsers_patch_list_option_repository(parser: argparse.ArgumentParser
patch-list command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "patch-list", "ahriman"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_patch_list_option_variable_empty(parser: argparse.ArgumentParser) -> None:
@ -420,10 +404,10 @@ def test_subparsers_patch_remove(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["patch-remove", "ahriman"])
assert args.action == Action.Remove
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_patch_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -431,7 +415,7 @@ def test_subparsers_patch_remove_option_architecture(parser: argparse.ArgumentPa
patch-remove command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-remove", "ahriman"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_patch_remove_option_repository(parser: argparse.ArgumentParser) -> None:
@ -439,7 +423,7 @@ def test_subparsers_patch_remove_option_repository(parser: argparse.ArgumentPars
patch-remove command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "patch-remove", "ahriman"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_patch_remove_option_variable_empty(parser: argparse.ArgumentParser) -> None:
@ -464,10 +448,10 @@ def test_subparsers_patch_set_add(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["patch-set-add", "ahriman"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.variable is None
@ -476,7 +460,7 @@ def test_subparsers_patch_set_add_option_architecture(parser: argparse.ArgumentP
patch-set-add command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "patch-set-add", "ahriman"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_patch_set_add_option_package(parser: argparse.ArgumentParser) -> None:
@ -492,7 +476,7 @@ def test_subparsers_patch_set_add_option_repository(parser: argparse.ArgumentPar
patch-set-add command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "patch-set-add", "ahriman"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_patch_set_add_option_track(parser: argparse.ArgumentParser) -> None:
@ -508,10 +492,10 @@ def test_subparsers_repo_backup(parser: argparse.ArgumentParser) -> None:
repo-backup command must imply architecture list, lock, report, repository and unsafe
"""
args = parser.parse_args(["repo-backup", "output.zip"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -520,7 +504,7 @@ def test_subparsers_repo_backup_option_architecture(parser: argparse.ArgumentPar
repo-backup command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "repo-backup", "output.zip"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_repo_backup_option_repository(parser: argparse.ArgumentParser) -> None:
@ -528,7 +512,7 @@ def test_subparsers_repo_backup_option_repository(parser: argparse.ArgumentParse
repo-backup command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "repo-backup", "output.zip"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_repo_check(parser: argparse.ArgumentParser) -> None:
@ -550,7 +534,7 @@ def test_subparsers_repo_check_option_architecture(parser: argparse.ArgumentPars
args = parser.parse_args(["repo-check"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-check"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_check_option_repository(parser: argparse.ArgumentParser) -> None:
@ -560,7 +544,7 @@ def test_subparsers_repo_check_option_repository(parser: argparse.ArgumentParser
args = parser.parse_args(["repo-check"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-check"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_check_option_refresh(parser: argparse.ArgumentParser) -> None:
@ -590,7 +574,7 @@ def test_subparsers_repo_create_keyring_option_architecture(parser: argparse.Arg
args = parser.parse_args(["repo-create-keyring"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-create-keyring"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_create_keyring_option_repository(parser: argparse.ArgumentParser) -> None:
@ -600,7 +584,7 @@ def test_subparsers_repo_create_keyring_option_repository(parser: argparse.Argum
args = parser.parse_args(["repo-create-keyring"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-create-keyring"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_create_mirrorlist(parser: argparse.ArgumentParser) -> None:
@ -618,7 +602,7 @@ def test_subparsers_repo_create_mirrorlist_option_architecture(parser: argparse.
args = parser.parse_args(["repo-create-mirrorlist"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-create-mirrorlist"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_create_mirrorlist_option_repository(parser: argparse.ArgumentParser) -> None:
@ -628,7 +612,7 @@ def test_subparsers_repo_create_mirrorlist_option_repository(parser: argparse.Ar
args = parser.parse_args(["repo-create-mirrorlist"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-create-mirrorlist"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_daemon(parser: argparse.ArgumentParser) -> None:
@ -670,7 +654,7 @@ def test_subparsers_repo_rebuild_option_architecture(parser: argparse.ArgumentPa
args = parser.parse_args(["repo-rebuild"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-rebuild"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_rebuild_option_repository(parser: argparse.ArgumentParser) -> None:
@ -680,7 +664,7 @@ def test_subparsers_repo_rebuild_option_repository(parser: argparse.ArgumentPars
args = parser.parse_args(["repo-rebuild"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-rebuild"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_rebuild_option_depends_on_empty(parser: argparse.ArgumentParser) -> None:
@ -714,7 +698,7 @@ def test_subparsers_repo_remove_unknown_option_architecture(parser: argparse.Arg
args = parser.parse_args(["repo-remove-unknown"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-remove-unknown"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_remove_unknown_option_repository(parser: argparse.ArgumentParser) -> None:
@ -724,7 +708,7 @@ def test_subparsers_repo_remove_unknown_option_repository(parser: argparse.Argum
args = parser.parse_args(["repo-remove-unknown"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-remove-unknown"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_report(parser: argparse.ArgumentParser) -> None:
@ -742,7 +726,7 @@ def test_subparsers_repo_report_option_architecture(parser: argparse.ArgumentPar
args = parser.parse_args(["repo-report"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-report"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_report_option_repository(parser: argparse.ArgumentParser) -> None:
@ -752,7 +736,7 @@ def test_subparsers_repo_report_option_repository(parser: argparse.ArgumentParse
args = parser.parse_args(["repo-report"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-report"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None:
@ -760,10 +744,10 @@ def test_subparsers_repo_restore(parser: argparse.ArgumentParser) -> None:
repo-restore command must imply architecture list, lock, report, repository and unsafe
"""
args = parser.parse_args(["repo-restore", "output.zip"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -772,7 +756,7 @@ def test_subparsers_repo_restore_option_architecture(parser: argparse.ArgumentPa
repo-restore command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "repo-restore", "output.zip"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_repo_restore_option_repository(parser: argparse.ArgumentParser) -> None:
@ -780,7 +764,7 @@ def test_subparsers_repo_restore_option_repository(parser: argparse.ArgumentPars
repo-restore command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "repo-restore", "output.zip"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_repo_sign_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -790,7 +774,7 @@ def test_subparsers_repo_sign_option_architecture(parser: argparse.ArgumentParse
args = parser.parse_args(["repo-sign"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-sign"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_sign_option_repository(parser: argparse.ArgumentParser) -> None:
@ -800,7 +784,7 @@ def test_subparsers_repo_sign_option_repository(parser: argparse.ArgumentParser)
args = parser.parse_args(["repo-sign"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-sign"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_status_update(parser: argparse.ArgumentParser) -> None:
@ -808,12 +792,12 @@ def test_subparsers_repo_status_update(parser: argparse.ArgumentParser) -> None:
re[p-status-update command must imply action, lock, quiet, report, package and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "package-status-update"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.action == Action.Update
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert not args.package
assert args.unsafe
@ -843,7 +827,7 @@ def test_subparsers_repo_sync_option_architecture(parser: argparse.ArgumentParse
args = parser.parse_args(["repo-sync"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-sync"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_sync_option_repository(parser: argparse.ArgumentParser) -> None:
@ -853,7 +837,7 @@ def test_subparsers_repo_sync_option_repository(parser: argparse.ArgumentParser)
args = parser.parse_args(["repo-sync"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-sync"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_tree(parser: argparse.ArgumentParser) -> None:
@ -874,7 +858,7 @@ def test_subparsers_repo_tree_option_architecture(parser: argparse.ArgumentParse
args = parser.parse_args(["repo-tree"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-tree"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_tree_option_repository(parser: argparse.ArgumentParser) -> None:
@ -884,7 +868,7 @@ def test_subparsers_repo_tree_option_repository(parser: argparse.ArgumentParser)
args = parser.parse_args(["repo-tree"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-tree"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_tree_option_partitions(parser: argparse.ArgumentParser) -> None:
@ -904,7 +888,7 @@ def test_subparsers_repo_triggers_option_architecture(parser: argparse.ArgumentP
args = parser.parse_args(["repo-triggers"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-triggers"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_triggers_option_repository(parser: argparse.ArgumentParser) -> None:
@ -914,7 +898,7 @@ def test_subparsers_repo_triggers_option_repository(parser: argparse.ArgumentPar
args = parser.parse_args(["repo-triggers"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-triggers"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_update_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -924,7 +908,7 @@ def test_subparsers_repo_update_option_architecture(parser: argparse.ArgumentPar
args = parser.parse_args(["repo-update"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "repo-update"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_repo_update_option_repository(parser: argparse.ArgumentParser) -> None:
@ -934,7 +918,7 @@ def test_subparsers_repo_update_option_repository(parser: argparse.ArgumentParse
args = parser.parse_args(["repo-update"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "repo-update"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_repo_update_option_refresh(parser: argparse.ArgumentParser) -> None:
@ -965,7 +949,7 @@ def test_subparsers_service_clean_option_architecture(parser: argparse.ArgumentP
args = parser.parse_args(["service-clean"])
assert args.architecture is None
args = parser.parse_args(["-a", "x86_64", "service-clean"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
def test_subparsers_service_clean_option_repository(parser: argparse.ArgumentParser) -> None:
@ -975,7 +959,7 @@ def test_subparsers_service_clean_option_repository(parser: argparse.ArgumentPar
args = parser.parse_args(["service-clean"])
assert args.repository is None
args = parser.parse_args(["-r", "repo", "service-clean"])
assert args.repository == ["repo"]
assert args.repository == "repo"
def test_subparsers_service_config(parser: argparse.ArgumentParser) -> None:
@ -983,24 +967,41 @@ def test_subparsers_service_config(parser: argparse.ArgumentParser) -> None:
service-config command must imply lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert args.unsafe
def test_subparsers_service_config_option_section_key(parser: argparse.ArgumentParser) -> None:
"""
service-config command must parse optional section and key arguments
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config"])
assert args.section is None
assert args.key is None
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config", "section"])
assert args.section == "section"
assert args.key is None
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config", "section", "key"])
assert args.section == "section"
assert args.key == "key"
def test_subparsers_service_config_validate(parser: argparse.ArgumentParser) -> None:
"""
service-config-validate command must imply lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-config-validate"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert args.unsafe
@ -1009,10 +1010,10 @@ def test_subparsers_service_key_import(parser: argparse.ArgumentParser) -> None:
service-key-import command must imply architecture list, lock, report and repository
"""
args = parser.parse_args(["service-key-import", "key"])
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_service_key_import_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -1020,7 +1021,7 @@ def test_subparsers_service_key_import_option_architecture(parser: argparse.Argu
service-key-import command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "service-key-import", "key"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_service_key_import_option_repository(parser: argparse.ArgumentParser) -> None:
@ -1028,7 +1029,35 @@ def test_subparsers_service_key_import_option_repository(parser: argparse.Argume
service-key-import command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "service-key-import", "key"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_service_repositories(parser: argparse.ArgumentParser) -> None:
"""
service-repositories command must imply architecture, lock, report, repository and unsafe
"""
args = parser.parse_args(["service-repositories"])
assert args.architecture == ""
assert args.lock is None
assert not args.report
assert args.repository == ""
assert args.unsafe
def test_subparsers_service_repositories_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
service-repositories command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "service-repositories"])
assert args.architecture == ""
def test_subparsers_service_repositories_option_repository(parser: argparse.ArgumentParser) -> None:
"""
service-repositories command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "service-repositories"])
assert args.repository == ""
def test_subparsers_service_setup(parser: argparse.ArgumentParser) -> None:
@ -1036,11 +1065,11 @@ def test_subparsers_service_setup(parser: argparse.ArgumentParser) -> None:
service-setup command must imply lock, quiet, report and unsafe
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "service-setup", "--packager", "John Doe <john@doe.com>"])
assert args.architecture == ["x86_64"]
assert args.architecture == "x86_64"
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == ["repo"]
assert args.repository == "repo"
assert args.unsafe
@ -1107,11 +1136,11 @@ def test_subparsers_user_add(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["user-add", "username"])
assert args.action == Action.Update
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_user_add_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -1119,7 +1148,7 @@ def test_subparsers_user_add_option_architecture(parser: argparse.ArgumentParser
user-add command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "user-add", "username"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_user_add_option_repository(parser: argparse.ArgumentParser) -> None:
@ -1127,7 +1156,7 @@ def test_subparsers_user_add_option_repository(parser: argparse.ArgumentParser)
user-add command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "user-add", "username"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_user_add_option_role(parser: argparse.ArgumentParser) -> None:
@ -1146,11 +1175,11 @@ def test_subparsers_user_list(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["user-list"])
assert args.action == Action.List
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
assert args.unsafe
@ -1159,7 +1188,7 @@ def test_subparsers_user_list_option_architecture(parser: argparse.ArgumentParse
user-list command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "user-list"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_user_list_option_repository(parser: argparse.ArgumentParser) -> None:
@ -1167,7 +1196,7 @@ def test_subparsers_user_list_option_repository(parser: argparse.ArgumentParser)
user-list command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "user-list"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_user_list_option_role(parser: argparse.ArgumentParser) -> None:
@ -1184,11 +1213,11 @@ def test_subparsers_user_remove(parser: argparse.ArgumentParser) -> None:
"""
args = parser.parse_args(["user-remove", "username"])
assert args.action == Action.Remove
assert args.architecture == [""]
assert args.architecture == ""
assert args.lock is None
assert args.quiet
assert not args.report
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_user_remove_option_architecture(parser: argparse.ArgumentParser) -> None:
@ -1196,7 +1225,7 @@ def test_subparsers_user_remove_option_architecture(parser: argparse.ArgumentPar
user-remove command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "user-remove", "username"])
assert args.architecture == [""]
assert args.architecture == ""
def test_subparsers_user_remove_option_repository(parser: argparse.ArgumentParser) -> None:
@ -1204,20 +1233,36 @@ def test_subparsers_user_remove_option_repository(parser: argparse.ArgumentParse
user-remove command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "user-remove", "username"])
assert args.repository == [""]
assert args.repository == ""
def test_subparsers_web(parser: argparse.ArgumentParser) -> None:
"""
web command must imply report and parser
web command must imply architecture, report, repository and parser
"""
args = parser.parse_args(["-a", "x86_64", "-r", "repo", "web"])
assert args.architecture == ["x86_64"]
args = parser.parse_args(["web"])
assert args.architecture == ""
assert not args.report
assert args.repository == ["repo"]
assert args.repository == ""
assert args.parser is not None and args.parser()
def test_subparsers_web_option_architecture(parser: argparse.ArgumentParser) -> None:
"""
web command must correctly parse architecture list
"""
args = parser.parse_args(["-a", "x86_64", "web"])
assert args.architecture == ""
def test_subparsers_web_option_repository(parser: argparse.ArgumentParser) -> None:
"""
web command must correctly parse repository list
"""
args = parser.parse_args(["-r", "repo", "web"])
assert args.repository == ""
def test_run(args: argparse.Namespace, configuration: Configuration, mocker: MockerFixture) -> None:
"""
application must be run

View File

@ -245,17 +245,19 @@ def auth(configuration: Configuration) -> Auth:
@pytest.fixture
def configuration(repository_id: RepositoryId, resource_path_root: Path) -> Configuration:
def configuration(repository_id: RepositoryId, resource_path_root: Path, mocker: MockerFixture) -> Configuration:
"""
configuration fixture
Args:
repository_id(RepositoryId): repository identifier fixture
resource_path_root(Path): resource path root directory
mocker(MockerFixture): mocker object
Returns:
Configuration: configuration test instance
"""
mocker.patch("ahriman.core.configuration.Configuration.load_includes")
path = resource_path_root / "core" / "ahriman.ini"
return Configuration.from_path(path, repository_id)
@ -541,10 +543,7 @@ def spawner(configuration: Configuration) -> Spawn:
Returns:
Spawn: spawner fixture
"""
_, repository_id = configuration.check_loaded()
return Spawn(MagicMock(), repository_id, [
"--architecture", "x86_64",
"--repository", repository_id.name,
return Spawn(MagicMock(), [
"--configuration", str(configuration.path),
])
@ -561,19 +560,15 @@ def user() -> User:
@pytest.fixture
def watcher(configuration: Configuration, database: SQLite, repository: Repository, mocker: MockerFixture) -> Watcher:
def watcher(repository_id: RepositoryId, database: SQLite, repository: Repository) -> Watcher:
"""
package status watcher fixture
Args:
configuration(Configuration): configuration fixture
repository_id(RepositoryId): repository identifier fixture
database(SQLite): database fixture
repository(Repository): repository fixture
mocker(MockerFixture): mocker object
Returns:
Watcher: package status watcher test instance
"""
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
_, repository_id = configuration.check_loaded()
return Watcher(repository_id, configuration, database)
return Watcher(repository_id, database)

View File

@ -6,7 +6,6 @@ from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
from ahriman.models.aur_package import AURPackage
@ -129,29 +128,28 @@ def test_aur_request_failed_http_error(aur: AUR, mocker: MockerFixture) -> None:
aur.aur_request("info", "ahriman")
def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
def test_package_info(aur: AUR, aur_package_ahriman: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for info
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.AUR.aur_request", return_value=[aur_package_ahriman])
assert aur.package_info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman
assert aur.package_info(aur_package_ahriman.name, pacman=None) == aur_package_ahriman
request_mock.assert_called_once_with("info", aur_package_ahriman.name)
def test_package_info_not_found(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
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
"""
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=pacman)
assert aur.package_info(aur_package_ahriman.name, pacman=None)
def test_package_search(aur: AUR, aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> 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=pacman) == [aur_package_ahriman]
assert aur.package_search(aur_package_ahriman.name, pacman=None) == [aur_package_ahriman]
request_mock.assert_called_once_with("search", aur_package_ahriman.name, by="name-desc")

View File

@ -6,7 +6,6 @@ from pathlib import Path
from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import Official
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
from ahriman.models.aur_package import AURPackage
@ -95,33 +94,30 @@ def test_arch_request_failed_http_error(official: Official, mocker: MockerFixtur
official.arch_request("akonadi", by="q")
def test_package_info(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
def test_package_info(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for info
"""
request_mock = mocker.patch("ahriman.core.alpm.remote.Official.arch_request",
return_value=[aur_package_akonadi])
assert official.package_info(aur_package_akonadi.name, pacman=pacman) == aur_package_akonadi
assert official.package_info(aur_package_akonadi.name, pacman=None) == aur_package_akonadi
request_mock.assert_called_once_with(aur_package_akonadi.name, by="name")
def test_package_info_not_found(official: Official, aur_package_ahriman: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
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
"""
mocker.patch("ahriman.core.alpm.remote.Official.arch_request", return_value=[])
with pytest.raises(UnknownPackageError, match=aur_package_ahriman.name):
assert official.package_info(aur_package_ahriman.name, pacman=pacman)
assert official.package_info(aur_package_ahriman.name, pacman=None)
def test_package_search(official: Official, aur_package_akonadi: AURPackage, pacman: Pacman,
mocker: MockerFixture) -> None:
def test_package_search(official: Official, aur_package_akonadi: AURPackage, mocker: MockerFixture) -> None:
"""
must make request for search
"""
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=pacman) == [aur_package_akonadi]
assert official.package_search(aur_package_akonadi.name, pacman=None) == [aur_package_akonadi]
request_mock.assert_called_once_with(aur_package_akonadi.name, by="q")

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