Compare commits

..

1 Commits

Author SHA1 Message Date
e119f092b4 docs: add check if docs are updated 2025-06-19 16:34:32 +03:00
157 changed files with 2112 additions and 4067 deletions

View File

@ -13,15 +13,7 @@ jobs:
runs-on: ubuntu-latest
container:
image: archlinux:base
options: -w /build
volumes:
- ${{ github.workspace }}:/build
steps:
- run: pacman --noconfirm -Syu base-devel git python-tox
- uses: actions/checkout@v4
- name: Extract version
@ -35,6 +27,10 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
filter: 'Release \d+\.\d+\.\d+'
- uses: ConorMacBride/install-package@v1.1.0
with:
apt: tox
- name: Create archive
run: tox -e archive
env:

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -100,14 +100,6 @@ ahriman.application.handlers.rebuild module
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.reload module
------------------------------------------
.. automodule:: ahriman.application.handlers.reload
:members:
:no-undoc-members:
:show-inheritance:
ahriman.application.handlers.remove module
------------------------------------------

View File

@ -1,21 +0,0 @@
ahriman.core.housekeeping package
=================================
Submodules
----------
ahriman.core.housekeeping.logs\_rotation\_trigger module
--------------------------------------------------------
.. automodule:: ahriman.core.housekeeping.logs_rotation_trigger
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.housekeeping
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -15,7 +15,6 @@ Subpackages
ahriman.core.distributed
ahriman.core.formatters
ahriman.core.gitremote
ahriman.core.housekeeping
ahriman.core.http
ahriman.core.log
ahriman.core.report

View File

@ -44,14 +44,6 @@ ahriman.web.schemas.changes\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.configuration\_schema module
------------------------------------------------
.. automodule:: ahriman.web.schemas.configuration_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.counters\_schema module
-------------------------------------------
@ -148,14 +140,6 @@ ahriman.web.schemas.logs\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.logs\_search\_schema module
-----------------------------------------------
.. automodule:: ahriman.web.schemas.logs_search_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.oauth2\_schema module
-----------------------------------------

View File

@ -12,14 +12,6 @@ ahriman.web.views.v1.service.add module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.config module
------------------------------------------
.. automodule:: ahriman.web.views.v1.service.config
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.v1.service.logs module
----------------------------------------

View File

@ -40,7 +40,6 @@ This package contains everything required for the most of application actions an
* ``ahriman.core.distributed`` package with triggers and helpers for distributed build system.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.housekeeping`` package provides few triggers for removing old data.
* ``ahriman.core.http`` package provides HTTP clients which can be used later by other classes.
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and some wrappers.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.

View File

@ -81,6 +81,7 @@ Base configuration settings.
* ``apply_migrations`` - perform database migrations on the 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 manually.
* ``database`` - path to the application SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
``alpm:*`` groups
@ -165,7 +166,6 @@ Reporting to web service related settings. In most cases there is fallback to we
Web server settings. 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.
* ``autorefresh_intervals`` - enable page auto refresh options, space separated list of integers, optional. The first defined interval will be used as default. If no intervals set, the auto refresh buttons will be disabled. If first element of the list equals ``0``, auto refresh will be disabled by default.
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
* ``host`` - host to bind, string, optional.
* ``index_url`` - full URL of the repository index page, string, optional.
@ -179,7 +179,7 @@ Web server settings. This feature requires ``aiohttp`` libraries to be installed
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, integer, optional.
``keyring`` group
-----------------
--------------------
Keyring package generator plugin.
@ -197,13 +197,6 @@ Keyring generator plugin
* ``revoked`` - list of revoked packagers keys, space separated list of strings, optional.
* ``trusted`` - list of master keys, space separated list of strings, optional, if not set, the ``key`` option from ``sign`` group will be used.
``housekeeping`` group
----------------------
This section describes settings for the ``ahriman.core.housekeeping.LogsRotationTrigger`` plugin.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
``mirrorlist`` group
--------------------

View File

@ -2,7 +2,7 @@
pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.19.0
pkgver=2.18.2
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
@ -40,7 +40,6 @@ package_ahriman-core() {
'rsync: sync by using rsync')
install="$pkgbase.install"
backup=('etc/ahriman.ini'
'etc/ahriman.ini.d/00-housekeeping.ini'
'etc/ahriman.ini.d/logging.ini')
cd "$pkgbase-$pkgver"
@ -50,7 +49,6 @@ package_ahriman-core() {
# keep usr/share configs as reference and copy them to /etc
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/00-housekeeping.ini" "$pkgdir/etc/ahriman.ini.d/00-housekeeping.ini"
install -Dm644 "$pkgdir/usr/share/$pkgbase/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"
install -Dm644 "$srcdir/$pkgbase.sysusers" "$pkgdir/usr/lib/sysusers.d/$pkgbase.conf"

View File

@ -5,7 +5,6 @@ After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/ahriman web
ExecReload=/usr/bin/ahriman web-reload
User=ahriman
Group=ahriman

View File

@ -7,6 +7,8 @@ logging = ahriman.ini.d/logging.ini
;apply_migrations = yes
; Path to the application SQLite database.
database = ${repository:root}/ahriman.db
; Keep last build logs for each package
keep_last_logs = 5
[alpm]
; Path to pacman system database cache.
@ -43,11 +45,9 @@ triggers[] = ahriman.core.gitremote.RemotePullTrigger
triggers[] = ahriman.core.report.ReportTrigger
triggers[] = ahriman.core.upload.UploadTrigger
triggers[] = ahriman.core.gitremote.RemotePushTrigger
triggers[] = ahriman.core.housekeeping.LogsRotationTrigger
; List of well-known triggers. Used only for configuration purposes.
triggers_known[] = ahriman.core.gitremote.RemotePullTrigger
triggers_known[] = ahriman.core.gitremote.RemotePushTrigger
triggers_known[] = ahriman.core.housekeeping.LogsRotationTrigger
triggers_known[] = ahriman.core.report.ReportTrigger
triggers_known[] = ahriman.core.upload.UploadTrigger
; Maximal age in seconds of the VCS packages before their version will be updated with its remote source.

View File

@ -1,3 +0,0 @@
[logs-rotation]
; Keep last build logs for each package
keep_last_logs = 5

View File

@ -28,10 +28,6 @@ allow_read_only = yes
; External address of the web service. Will be used for some features like OAuth. If none set will be generated as
; address = http://${web:host}:${web:port}
;address = http://${web:host}:${web:port}
; Enable page auto refresh. Intervals are given in seconds. Default interval is always the first element of the list.
; If no intervals set, auto refresh will be disabled. 0 can only be the first element and will disable auto refresh
; by default.
autorefresh_intervals = 5 1 10 30 60
; Enable file upload endpoint used by some triggers.
;enable_archive_upload = no
; Address to bind the server.

View File

@ -55,11 +55,6 @@
<i class="bi bi-play"></i> update
</button>
</li>
<li>
<button id="update-repositories-button" class="btn dropdown-item" onclick="refreshDatabases()">
<i class="bi bi-arrow-down-circle"></i> update pacman databases
</button>
</li>
<li>
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal">
<i class="bi bi-arrow-clockwise"></i> rebuild
@ -80,28 +75,10 @@
<button type="button" class="btn btn-secondary" onclick="reload()">
<i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span>
</button>
{% if autorefresh_intervals %}
<div class="btn-group">
<input id="table-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="toggleTableAutoReload()" checked>
<label for="table-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">select interval</span>
</button>
<ul id="table-autoreload-input" class="dropdown-menu">
{% for interval in autorefresh_intervals %}
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="toggleTableAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<table id="packages"
data-classes="table table-hover"
data-cookie="true"
data-cookie-id-table="ahriman-packages"
data-cookie-storage="localStorage"
data-export-options='{"fileName": "packages"}'
data-filter-control="true"
data-filter-control-visible="false"
@ -120,8 +97,8 @@
data-sortable="true"
data-sort-name="base"
data-sort-order="asc"
data-toolbar="#toolbar"
data-unique-id="id">
data-toggle="table"
data-toolbar="#toolbar">
<thead class="table-primary">
<tr>
<th data-checkbox="true"></th>

View File

@ -3,9 +3,7 @@
function createAlert(title, message, clz, action, id) {
id ??= md5(title + message); // MD5 id from the content
if (alertPlaceholder.querySelector(`#alert-${id}`)) {
return; // check if there are duplicates
}
if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates
const wrapper = document.createElement("div");
wrapper.id = `alert-${id}`;

View File

@ -51,87 +51,6 @@
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
let dashboardPackagesCountChart = null;
function statusLoad() {
const badgeClass = status => {
if (status === "pending") return "btn-outline-warning";
if (status === "building") return "btn-outline-warning";
if (status === "failed") return "btn-outline-danger";
if (status === "success") return "btn-outline-success";
return "btn-outline-secondary";
};
makeRequest(
"/api/v1/status",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
dashboardButton.classList.remove(...dashboardButton.classList);
dashboardButton.classList.add("btn");
dashboardButton.classList.add(badgeClass(data.status.status));
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
dashboardModalHeader.classList.add("modal-header");
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
dashboardName.textContent = data.repository;
dashboardArchitecture.textContent = data.architecture;
dashboardStatus.textContent = data.status.status;
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
if (dashboardPackagesStatusesChart) {
const labels = [
"unknown",
"pending",
"building",
"failed",
"success",
];
dashboardPackagesStatusesChart.config.data = {
labels: labels,
datasets: [{
label: "packages in status",
data: labels.map(label => data.packages[label]),
backgroundColor: [
"rgb(55, 58, 60)",
"rgb(255, 117, 24)",
"rgb(255, 117, 24)",
"rgb(255, 0, 57)",
"rgb(63, 182, 24)", // copy-paste from current style
],
}],
};
dashboardPackagesStatusesChart.update();
}
if (dashboardPackagesCountChart) {
dashboardPackagesCountChart.config.data = {
labels: ["packages"],
datasets: [
{
label: "archives",
data: [data.stats.packages],
},
{
label: "bases",
data: [data.stats.bases],
},
],
};
dashboardPackagesCountChart.update();
}
dashboardCanvas.hidden = data.status.total > 0;
},
);
}
ready(_ => {
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
type: "pie",

View File

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

View File

@ -80,7 +80,8 @@
data-classes="table table-hover"
data-sortable="true"
data-sort-name="timestamp"
data-sort-order="desc">
data-sort-order="desc"
data-toggle="table">
<thead class="table-primary">
<tr>
<th data-align="right" data-field="timestamp">date</th>
@ -94,27 +95,10 @@
</div>
<div class="modal-footer">
{% if not auth.enabled or auth.username is not none %}
<input id="package-info-refresh-input" type="checkbox" class="form-check-input" value="" checked>
<label for="package-info-refresh-input" class="form-check-label">update pacman databases</label>
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
{% endif %}
{% if autorefresh_intervals %}
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
<div class="btn-group dropup">
<input id="package-info-autoreload-button" type="checkbox" class="btn-check" autocomplete="off" onclick="togglePackageInfoAutoReload()" checked>
<label for="package-info-autoreload-button" class="btn btn-outline-secondary" title="toggle auto reload"><i class="bi bi-clock"></i></label>
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">select interval</span>
</button>
<ul id="package-info-autoreload-input" class="dropdown-menu">
{% for interval in autorefresh_intervals %}
<li><a class="dropdown-item {{ "active" if interval.is_active }}" onclick="togglePackageInfoAutoReload({{ interval.interval }})" data-interval="{{ interval.interval }}">{{ interval.text }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button>
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
</div>
</div>
@ -151,12 +135,6 @@
const packageInfoVariablesBlock = document.getElementById("package-info-variables-block");
const packageInfoVariablesDiv = document.getElementById("package-info-variables-div");
const packageInfoRefreshInput = document.getElementById("package-info-refresh-input");
const packageInfoAutoReloadButton = document.getElementById("package-info-autoreload-button");
const packageInfoAutoReloadInput = document.getElementById("package-info-autoreload-input");
let packageInfoAutoReloadTask = null;
function clearChart() {
packageInfoEventsUpdateChartCanvas.hidden = true;
if (packageInfoEventsUpdateChart) {
@ -165,13 +143,6 @@
}
}
function convertLogs(data, filter) {
return data
.filter((filter || Boolean))
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`)
.join("\n");
}
async function copyChanges() {
const changes = packageInfoChangesInput.textContent;
await copyToClipboard(changes, packageInfoChangesCopyButton);
@ -315,69 +286,6 @@
}
function loadLogs(packageBase, onFailure) {
const sortFn = (left, right) => left.process_id.localeCompare(right.process_id) || left.version.localeCompare(right.version);
const compareFn = (left, right) => left.process_id === right.process_id && left.version === right.version;
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
query: {
architecture: repository.architecture,
head: true,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
const currentVersions = Array.from(packageInfoLogsVersions.children)
.map(el => {
return {
process_id: el.dataset.processId,
version: el.dataset.version,
};
})
.sort(sortFn);
const newVersions = data
.map(el => {
return {
process_id: el.process_id,
version: el.version,
};
})
.sort(sortFn);
if (currentVersions.equals(newVersions, compareFn))
loadLogsActive(packageBase);
else
loadLogsAll(packageBase, onFailure);
},
)
}
function loadLogsActive(packageBase) {
const activeLogSelector = packageInfoLogsVersions.querySelector(".active");
if (activeLogSelector) {
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
query: {
architecture: repository.architecture,
repository: repository.repository,
version: activeLogSelector.dataset.version,
process_id: activeLogSelector.dataset.processId,
},
convert: response => response.json(),
},
data => {
activeLogSelector.dataset.logs = convertLogs(data);
activeLogSelector.click();
},
);
}
}
function loadLogsAll(packageBase, onFailure) {
makeRequest(
`/api/v2/packages/${packageBase}/logs`,
{
@ -406,19 +314,15 @@
const link = document.createElement("a");
link.classList.add("dropdown-item");
link.dataset.version = version.version;
link.dataset.processId = version.process_id;
link.dataset.logs = convertLogs(data, log_record => log_record.version === version.version && log_record.process_id === version.process_id);
link.textContent = new Date(1000 * version.created).toISOStringShort();
link.href = "#";
link.onclick = _ => {
// check if we are at the bottom of the code block
const isScrolledToBottom = packageInfoLogsInput.scrollTop + packageInfoLogsInput.clientHeight >= packageInfoLogsInput.scrollHeight;
packageInfoLogsInput.textContent = link.dataset.logs;
const logs = data
.filter(log_record => log_record.version === version.version && log_record.process_id === version.process_id)
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`);
packageInfoLogsInput.textContent = logs.join("\n");
highlight(packageInfoLogsInput);
if (isScrolledToBottom)
packageInfoLogsInput.scrollTop = packageInfoLogsInput.scrollHeight; // scroll to the new end
Array.from(packageInfoLogsVersions.children).forEach(el => el.classList.remove("active"));
link.classList.add("active");
@ -494,23 +398,23 @@
}
function packageInfoRemove() {
const packageBase = packageInfoModal.dataset.package;
const packageBase = packageInfoModal.package;
packagesRemove([packageBase]);
}
function packageInfoUpdate() {
const packageBase = packageInfoModal.dataset.package;
packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked});
const packageBase = packageInfoModal.package;
packagesAdd(packageBase, [], repository);
}
function showPackageInfo(packageBase) {
const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet) {
// set package base as currently used
packageInfoModal.dataset.package = packageBase;
packageInfoModal.package = packageBase;
} else {
// read package base from the current window attribute
packageBase = packageInfoModal.dataset.package;
packageBase = packageInfoModal.package;
}
const onFailure = error => {
@ -529,27 +433,10 @@
if (isPackageBaseSet) {
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
{% if autorefresh_intervals %}
togglePackageInfoAutoReload();
{% endif %}
}
}
function togglePackageInfoAutoReload(interval) {
clearInterval(packageInfoAutoReloadTask);
packageInfoAutoReloadTask = toggleAutoReload(packageInfoAutoReloadButton, interval, packageInfoAutoReloadInput, _ => {
if (!hasActiveSelection()) {
const packageBase = packageInfoModal.dataset.package;
// we only poll status and logs here
loadPackage(packageBase);
loadLogs(packageBase);
}
});
}
ready(_ => {
packageInfoEventsTable.bootstrapTable({});
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
type: "line",
data: {},
@ -576,11 +463,6 @@
packageInfoChangesInput.textContent = "";
packageInfoEventsTable.bootstrapTable("load", []);
clearChart();
clearInterval(packageInfoAutoReloadTask);
packageInfoAutoReloadTask = null; // not really required (?) but lets clear everything
});
restoreAutoReloadSettings(packageInfoAutoReloadButton, packageInfoAutoReloadInput);
});
</script>

View File

@ -10,10 +10,6 @@
const dashboardButton = document.getElementById("dashboard-button");
const versionBadge = document.getElementById("badge-version");
const tableAutoReloadButton = document.getElementById("table-autoreload-button");
const tableAutoReloadInput = document.getElementById("table-autoreload-input");
let tableAutoReloadTask = null;
function doPackageAction(uri, packages, repository, successText, failureText, data) {
makeRequest(
uri,
@ -59,41 +55,6 @@
return table.bootstrapTable("getSelections").map(row => row.id);
}
function packagesLoad(onFailure) {
makeRequest(
"/api/v1/packages",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
const payload = data
.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote.web_url;
return {
id: package_base,
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
status: description.status.status,
};
});
updateTable(table, payload);
table.bootstrapTable("hideLoading");
},
onFailure,
);
}
function packagesRemove(packages) {
packages = packages ?? getSelection();
const onSuccess = update => `Packages ${update} have been removed`;
@ -112,37 +73,132 @@
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
}
function refreshDatabases() {
const onSuccess = _ => "Pacman database update has been requested";
const onFailure = error => `Could not update pacman databases: ${error}`;
const parameters = {
refresh: true,
aur: false,
local: false,
manual: false,
};
doPackageAction("/api/v1/service/update", [], repository, onSuccess, onFailure, parameters);
}
function reload() {
table.bootstrapTable("showLoading");
const onFailure = error => {
if ((error.status === 401) || (error.status === 403)) {
// authorization error
const text = "In order to see statuses you must login first.";
table.find("tr.unauthorized").remove();
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
table.bootstrapTable("hideLoading");
} else {
// other errors
const message = details => `Could not load list of packages: ${details}`;
showFailure("Load failure", message, error);
}
const badgeClass = status => {
if (status === "pending") return "btn-outline-warning";
if (status === "building") return "btn-outline-warning";
if (status === "failed") return "btn-outline-danger";
if (status === "success") return "btn-outline-success";
return "btn-outline-secondary";
};
packagesLoad(onFailure);
statusLoad();
makeRequest(
"/api/v1/packages",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
const payload = data.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote.web_url;
return {
id: package_base,
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
version: safe(description.package.version),
packager: description.package.packager ? safe(description.package.packager) : "",
packages: listToTable(Object.keys(description.package.packages)),
groups: listToTable(extractListProperties(description.package, "groups")),
licenses: listToTable(extractListProperties(description.package, "licenses")),
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
status: description.status.status,
};
});
table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading");
},
error => {
if ((error.status === 401) || (error.status === 403)) {
// authorization error
const text = "In order to see statuses you must login first.";
table.find("tr.unauthorized").remove();
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
table.bootstrapTable("hideLoading");
} else {
// other errors
const message = details => `Could not load list of packages: ${details}`;
showFailure("Load failure", message, error);
}
},
);
makeRequest(
"/api/v1/status",
{
query: {
architecture: repository.architecture,
repository: repository.repository,
},
convert: response => response.json(),
},
data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
dashboardButton.classList.remove(...dashboardButton.classList);
dashboardButton.classList.add("btn");
dashboardButton.classList.add(badgeClass(data.status.status));
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
dashboardModalHeader.classList.add("modal-header");
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
dashboardName.textContent = data.repository;
dashboardArchitecture.textContent = data.architecture;
dashboardStatus.textContent = data.status.status;
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
if (dashboardPackagesStatusesChart) {
const labels = [
"unknown",
"pending",
"building",
"failed",
"success",
];
dashboardPackagesStatusesChart.config.data = {
labels: labels,
datasets: [{
label: "packages in status",
data: labels.map(label => data.packages[label]),
backgroundColor: [
"rgb(55, 58, 60)",
"rgb(255, 117, 24)",
"rgb(255, 117, 24)",
"rgb(255, 0, 57)",
"rgb(63, 182, 24)", // copy-paste from current style
],
}],
};
dashboardPackagesStatusesChart.update();
}
if (dashboardPackagesCountChart) {
dashboardPackagesCountChart.config.data = {
labels: ["packages"],
datasets: [
{
label: "archives",
data: [data.stats.packages],
},
{
label: "bases",
data: [data.stats.bases],
},
],
};
dashboardPackagesCountChart.update();
}
dashboardCanvas.hidden = data.status.total > 0;
},
);
}
function selectRepository() {
@ -161,24 +217,7 @@
return {classes: cellClass(value)};
}
function toggleTableAutoReload(interval) {
clearInterval(tableAutoReloadTask);
tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => {
if (!hasActiveModal() &&
!hasActiveDropdown()) {
packagesLoad();
statusLoad();
}
});
}
ready(_ => {
const onCheckFunction = function () {
if (packageRemoveButton) {
packageRemoveButton.disabled = !getSelection().length;
}
};
document.querySelectorAll("#repositories a").forEach(element => {
element.onclick = _ => {
repository = {
@ -193,55 +232,49 @@
};
});
table.bootstrapTable({
onCheck: onCheckFunction,
onCheckAll: onCheckFunction,
onClickRow: (data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id);
},
onCreatedControls: _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
},
onUncheck: onCheckFunction,
onUncheckAll: onCheckFunction,
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => {
if (packageRemoveButton) {
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length;
}
});
table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id);
});
table.on("created-controls.bs.table", _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
});
restoreAutoReloadSettings(tableAutoReloadButton, tableAutoReloadInput);
selectRepository();
{% if autorefresh_intervals %}
toggleTableAutoReload();
{% endif %}
});
</script>

View File

@ -53,7 +53,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-show-search-clear-button="true"
data-sortable="true"
data-sort-name="base"
data-sort-order="asc">
data-sort-order="asc"
data-toggle="table">
<thead class="table-primary">
<tr>
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
@ -127,38 +128,36 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
}
ready(_ => {
table.bootstrapTable({
onCreatedControls: _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
},
table.on("created-controls.bs.table", _ => {
new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: {
cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
},
});
});
});
</script>

View File

@ -1,24 +1,23 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.33.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.30.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/cookie/bootstrap-table-cookie.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script>
async function copyToClipboard(text, button) {
@ -59,20 +58,6 @@
return value.includes(dataList[index].toLowerCase());
}
function hasActiveSelection() {
return !document.getSelection().isCollapsed; // not sure if it is a valid way, but I guess so
}
function hasActiveDropdown() {
return Array.from(document.querySelectorAll(".dropdown-menu"))
.some(el => el.classList.contains("show"));
}
function hasActiveModal() {
return Array.from(document.querySelectorAll(".modal"))
.some(el => el.classList.contains("show"));
}
function headerClass(status) {
if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"];
@ -121,12 +106,6 @@
.catch(error => onFailure && onFailure(error));
}
function readOptional(extractor, callback) {
for (let value = extractor(); !!value; value = null) {
callback(value);
}
}
function ready(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1);
@ -135,11 +114,6 @@
}
}
function restoreAutoReloadSettings(toggle, intervalSelector) {
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-enabled`), value => toggle.checked = value === "true");
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-interval`), value => toggleActiveElement(intervalSelector, "interval", value));
}
function safe(string) {
return String(string)
.replace(/&/g, "&amp;")
@ -159,86 +133,7 @@
return element;
}
function toggleActiveElement(selector, dataType, value) {
const targetElement = selector.querySelector(`a[data-${dataType}="${value}"]`);
if (targetElement?.classList?.contains("active")) {
return; // element is already active, skip processing
}
Array.from(selector.children).forEach(il => {
Array.from(il.children).forEach(el => el.classList.remove("active"));
});
targetElement?.classList?.add("active");
}
function toggleAutoReload(toggle, interval, intervalSelector, callback) {
if (interval) {
toggle.checked = true; // toggle reload
} else {
interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element
}
let intervalId = null;
if (interval) {
if (toggle.checked) {
// refresh UI
toggleActiveElement(intervalSelector, "interval", interval);
// finally create timer task
intervalId = setInterval(callback, interval);
}
} else {
toggle.checked = false; // no active interval found, disable toggle
}
localStorage.setItem(`ahriman-${toggle.id}-refresh-enabled`, toggle.checked);
localStorage.setItem(`ahriman-${toggle.id}-refresh-interval`, interval);
return intervalId;
}
function updateTable(table, rows) {
// instead of using load method here, we just update rows manually to avoid table reinitialization
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
accumulator[row.id] = row["0"];
return accumulator;
}, {});
// insert or update rows
rows.forEach(row => {
if (Object.hasOwn(currentData, row.id)) {
row["0"] = currentData[row.id]; // copy checkbox state
table.bootstrapTable("updateByUniqueId", {
id: row.id,
row: row,
replace: true,
});
} else {
table.bootstrapTable("insertRow", {index: 0, row: row});
}
});
// remove old rows
const newData = rows.map(value => value.id);
Object.keys(currentData).forEach(id => {
if (!newData.includes(id)) {
table.bootstrapTable("removeByUniqueId", id);
}
});
}
Array.prototype.equals = function (right, comparator) {
let index = this.length;
if (index !== right.length) {
return false;
}
while (index--) {
if (!comparator(this[index], right[index])) {
return false;
}
}
return true;
}
Date.prototype.toISOStringShort = function () {
Date.prototype.toISOStringShort = function() {
const pad = number => String(number).padStart(2, "0");
return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`;
}

View File

@ -1,15 +1,15 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<style>
.pre-scrollable {

View File

@ -1,6 +1,6 @@
.TH AHRIMAN "1" "2025\-06\-29" "ahriman 2.19.0" "ArcH linux ReposItory MANager"
.TH AHRIMAN "1" "2025\-06\-16" "ahriman" "Generated Python Manual"
.SH NAME
ahriman \- ArcH linux ReposItory MANager
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] [-V] [--wait-timeout WAIT_TIMEOUT] {add,aur-search,check,clean,config,config-validate,copy,daemon,help,help-commands-unsafe,help-updates,help-version,init,key-import,package-add,package-changes,package-changes-remove,package-copy,package-remove,package-status,package-status-remove,package-status-update,package-update,patch-add,patch-list,patch-remove,patch-set-add,rebuild,remove,remove-unknown,repo-backup,repo-check,repo-clean,repo-config,repo-config-validate,repo-create-keyring,repo-create-mirrorlist,repo-daemon,repo-init,repo-rebuild,repo-remove-unknown,repo-report,repo-restore,repo-setup,repo-sign,repo-statistics,repo-status-update,repo-sync,repo-tree,repo-triggers,repo-update,report,run,search,service-clean,service-config,service-config-validate,service-key-import,service-repositories,service-run,service-setup,service-shell,service-tree-migrate,setup,shell,sign,status,status-update,sync,update,user-add,user-list,user-remove,version,web} ...

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,70 +0,0 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import argparse
from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.models.repository_id import RepositoryId
class Reload(Handler):
"""
web server reload 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
"""
application = Application(repository_id, configuration, report=True)
client = application.repository.reporter
client.configuration_reload()
@staticmethod
def _set_web_reload_parser(root: SubParserAction) -> argparse.ArgumentParser:
"""
add parser for web reload subcommand
Args:
root(SubParserAction): subparsers for the commands
Returns:
argparse.ArgumentParser: created argument parser
"""
parser = root.add_parser("web-reload", help="reload configuration",
description="reload web server configuration",
epilog="This method forces the web server to reload its configuration. "
"Note, however, that this method does not apply all configuration changes "
"(like ports, authentication, etc)")
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
return parser
arguments = [_set_web_reload_parser]

View File

@ -28,7 +28,6 @@ from ahriman.core.alpm.remote import AUR, Official
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import OptionError
from ahriman.core.formatters import AurPrinter
from ahriman.core.types import Comparable
from ahriman.models.aur_package import AURPackage
from ahriman.models.repository_id import RepositoryId
@ -116,7 +115,7 @@ class Search(Handler):
raise OptionError(sort_by)
# always sort by package name at the last
# well technically it is not a string, but we can deal with it
comparator: Callable[[AURPackage], Comparable] = \
comparator: Callable[[AURPackage], tuple[str, str]] =\
lambda package: (getattr(package, sort_by), package.name)
return sorted(packages, key=comparator)

View File

@ -72,17 +72,16 @@ class Setup(Handler):
application = Application(repository_id, configuration, report=report)
with application.repository.paths.preserve_owner():
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, repository_id)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(
repository_id, args.from_configuration, args.mirror, args.multilib, repository_server)
Setup.configuration_create_sudo(application.repository.paths, repository_id)
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
application.repository.repo.init()
# lazy database sync
application.repository.pacman.handle # pylint: disable=pointless-statement
@staticmethod
def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
@ -281,5 +280,6 @@ class Setup(Handler):
command = Setup.build_command(paths.root, repository_id)
command.unlink(missing_ok=True)
command.symlink_to(Setup.ARCHBUILD_COMMAND_PATH)
paths.chown(command) # we would like to keep owner inside ahriman's home
arguments = [_set_service_setup_parser]

View File

@ -25,7 +25,6 @@ from ahriman.application.application import Application
from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import PackagePrinter, StatusPrinter
from ahriman.core.types import Comparable
from ahriman.core.utils import enum_values
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.package import Package
@ -65,7 +64,7 @@ class Status(Handler):
Status.check_status(args.exit_code, packages)
comparator: Callable[[tuple[Package, BuildStatus]], Comparable] = lambda item: item[0].base
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda item: item[0].base
filter_fn: Callable[[tuple[Package, BuildStatus]], bool] =\
lambda item: args.status is None or item[1].status == args.status
for package, package_status in sorted(filter(filter_fn, packages), key=comparator):

View File

@ -130,8 +130,8 @@ class Pacman(LazyLogging):
return # database for some reason deos not exist
self.logger.info("copy pacman database %s from operating system root to ahriman's home %s", src, dst)
with self.repository_paths.preserve_owner(dst.parent):
shutil.copy(src, dst)
shutil.copy(src, dst)
self.repository_paths.chown(dst)
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
"""
@ -255,20 +255,3 @@ class Pacman(LazyLogging):
result.update(trim_package(provides) for provides in package.provides)
return result
def provided_by(self, package_name: str) -> Generator[Package, None, None]:
"""
search through databases and emit packages which provides the ``package_name``
Args:
package_name(str): package name to search
Yields:
Package: list of packages which were returned by the query
"""
def is_package_provided(package: Package) -> bool:
provides = [trim_package(name) for name in package.provides]
return package_name in provides
for database in self.handle.get_syncdbs():
yield from filter(is_package_provided, database.search(package_name))

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# pylint: disable=too-many-public-methods
import configparser
import shlex
import sys
@ -86,10 +85,9 @@ class Configuration(configparser.RawConfigParser):
empty_lines_in_values=not allow_multi_key,
interpolation=ShellInterpolator(),
converters={
"intlist": lambda value: list(map(int, shlex.split(value))),
"list": shlex.split,
"path": self._convert_path,
"pathlist": lambda value: list(map(self._convert_path, shlex.split(value))),
"pathlist": lambda value: [self._convert_path(element) for element in shlex.split(value)],
},
)
@ -212,17 +210,6 @@ class Configuration(configparser.RawConfigParser):
raise InitializeError("Configuration path and/or repository id are not set")
return self.path, self.repository_id
def copy_from(self, configuration: Self) -> None:
"""
copy values from another instance overriding existing
Args:
configuration(Self): configuration instance to merge from
"""
for section in configuration.sections():
for key, value in configuration.items(section):
self.set_option(section, key, value)
def dump(self) -> dict[str, dict[str, str]]:
"""
dump configuration to dictionary
@ -233,13 +220,10 @@ class Configuration(configparser.RawConfigParser):
return {
section: dict(self.items(section))
for section in self.sections()
if self[section]
}
# pylint and mypy are too stupid to find these methods
# pylint: disable=missing-function-docstring,unused-argument
def getintlist(self, *args: Any, **kwargs: Any) -> list[int]: ... # type: ignore[empty-body]
def getlist(self, *args: Any, **kwargs: Any) -> list[str]: ... # type: ignore[empty-body]
def getpath(self, *args: Any, **kwargs: Any) -> Path: ... # type: ignore[empty-body]

View File

@ -45,6 +45,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"path_exists": True,
"path_type": "dir",
},
"keep_last_logs": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"logging": {
"type": "path",
"coerce": "absolute_path",
@ -319,15 +324,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False,
"is_url": ["http", "https"],
},
"autorefresh_intervals": {
"type": "list",
"coerce": "list",
"schema": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
"enable_archive_upload": {
"type": "boolean",
"coerce": "boolean",

View File

@ -29,15 +29,13 @@ class LogsOperations(Operations):
logs operations
"""
def logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[LogRecord]:
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
repository_id: RepositoryId | None = None) -> list[LogRecord]:
"""
extract logs for specified package base
Args:
package_base(str): package base to extract logs
version(str | None, optional): package version to filter (Default value = None)
process_id(str | None, optional): process identifier to filter (Default value = None)
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)
@ -54,17 +52,12 @@ class LogsOperations(Operations):
"""
select created, message, version, process_id from (
select * from logs
where package_base = :package_base
and repository = :repository
and (:version is null or version = :version)
and (:process_id is null or process_id = :process_id)
where package_base = :package_base and repository = :repository
order by created desc limit :limit offset :offset
) order by created asc
""",
{
"package_base": package_base,
"version": version,
"process_id": process_id,
"repository": repository_id.id,
"limit": limit,
"offset": offset,

View File

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

View File

@ -1,20 +0,0 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core.housekeeping.logs_rotation_trigger import LogsRotationTrigger

View File

@ -1,87 +0,0 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman.core import context
from ahriman.core.configuration import Configuration
from ahriman.core.status import Client
from ahriman.core.triggers import Trigger
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result
class LogsRotationTrigger(Trigger):
"""
rotate logs after build processes
Attributes:
keep_last_records(int): number of last records to keep
"""
CONFIGURATION_SCHEMA = {
"logs-rotation": {
"type": "dict",
"schema": {
"keep_last_logs": {
"type": "integer",
"required": True,
"coerce": "integer",
"min": 0,
},
},
},
}
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
"""
Args:
repository_id(RepositoryId): repository unique identifier
configuration(Configuration): configuration instance
"""
Trigger.__init__(self, repository_id, configuration)
section = next(iter(self.configuration_sections(configuration)))
self.keep_last_records = configuration.getint( # read old-style first and then fallback to new style
"settings", "keep_last_logs",
fallback=configuration.getint(section, "keep_last_logs"))
@classmethod
def configuration_sections(cls, configuration: Configuration) -> list[str]:
"""
extract configuration sections from configuration
Args:
configuration(Configuration): configuration instance
Returns:
list[str]: read configuration sections belong to this trigger
"""
return list(cls.CONFIGURATION_SCHEMA.keys())
def on_result(self, result: Result, packages: list[Package]) -> None:
"""
run trigger
Args:
result(Result): build result
packages(list[Package]): list of all available packages
"""
ctx = context.get()
reporter = ctx.get(Client)
reporter.logs_rotate(self.keep_last_records)

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import requests
import sys
from functools import cached_property
from typing import Any, IO, Literal
@ -71,10 +70,7 @@ class SyncHttpClient(LazyLogging):
request.Session: created session object
"""
session = requests.Session()
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
session.headers["User-Agent"] = f"ahriman/{__version__} " \
f"{requests.utils.default_user_agent()} " \
f"python/{python_version}"
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session

View File

@ -17,6 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import atexit
import logging
import uuid
@ -36,6 +37,7 @@ class HttpLogHandler(logging.Handler):
method
Attributes:
keep_last_records(int): number of last records to keep
reporter(Client): build status reporter instance
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
"""
@ -54,6 +56,7 @@ class HttpLogHandler(logging.Handler):
self.reporter = Client.load(repository_id, configuration, report=report)
self.suppress_errors = suppress_errors
self.keep_last_records = configuration.getint("settings", "keep_last_logs", fallback=0)
@classmethod
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
@ -80,6 +83,7 @@ class HttpLogHandler(logging.Handler):
root.addHandler(handler)
LogRecordId.DEFAULT_PROCESS_ID = str(uuid.uuid4()) # assign default process identifier for log records
atexit.register(handler.rotate)
return handler
@ -100,3 +104,9 @@ class HttpLogHandler(logging.Handler):
if self.suppress_errors:
return
self.handleError(record)
def rotate(self) -> None:
"""
rotate log records, removing older ones
"""
self.reporter.logs_rotate(self.keep_last_records)

View File

@ -26,7 +26,6 @@ from typing import Any
from ahriman.core.configuration import Configuration
from ahriman.core.sign.gpg import GPG
from ahriman.core.types import Comparable
from ahriman.core.utils import pretty_datetime, pretty_size, utcnow
from ahriman.models.repository_id import RepositoryId
from ahriman.models.result import Result
@ -112,7 +111,7 @@ class JinjaTemplate:
Returns:
list[dict[str, str]]: sorted content according to comparator defined
"""
comparator: Callable[[dict[str, str]], Comparable] = lambda item: item["filename"]
comparator: Callable[[dict[str, str]], str] = lambda item: item["filename"]
return sorted(content, key=comparator)
def make_html(self, result: Result, template_name: Path | str) -> str:

View File

@ -28,7 +28,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.status import Client
from ahriman.core.types import Comparable
from ahriman.models.event import EventType
from ahriman.models.package import Package
from ahriman.models.repository_id import RepositoryId
@ -87,7 +86,7 @@ class RSS(Report, JinjaTemplate):
Returns:
list[dict[str, str]]: sorted content according to comparator defined
"""
comparator: Callable[[dict[str, str]], Comparable] = \
comparator: Callable[[dict[str, str]], datetime.datetime] = \
lambda item: parsedate_to_datetime(item["build_date"])
return sorted(content, key=comparator, reverse=True)

View File

@ -81,11 +81,6 @@ class Client:
return make_local_client()
def configuration_reload(self) -> None:
"""
reload configuration
"""
def event_add(self, event: Event) -> None:
"""
create new event
@ -208,15 +203,12 @@ class Client:
"""
# this method does not raise NotImplementedError because it is actively used as dummy client for http log
def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
limit: int = -1, offset: int = 0) -> list[LogRecord]:
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
"""
get package logs
Args:
package_base(str): package base
version(str | None, optional): package version to search (Default value = None)
process_id(str | None, optional): process identifier to search (Default value = None)
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)

View File

@ -152,22 +152,19 @@ class LocalClient(Client):
"""
self.database.logs_insert(log_record, self.repository_id)
def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
limit: int = -1, offset: int = 0) -> list[LogRecord]:
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
"""
get package logs
Args:
package_base(str): package base
version(str | None, optional): package version to search (Default value = None)
process_id(str | None, optional): process identifier to search (Default value = None)
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
Returns:
list[LogRecord]: package logs
"""
return self.database.logs_get(package_base, version, process_id, limit, offset, self.repository_id)
return self.database.logs_get(package_base, limit, offset, self.repository_id)
def package_logs_remove(self, package_base: str, version: str | None) -> None:
"""

View File

@ -109,7 +109,7 @@ class Watcher(LazyLogging):
package_logs_add: Callable[[LogRecord], None]
package_logs_get: Callable[[str, str | None, str | None, int, int], list[LogRecord]]
package_logs_get: Callable[[str, int, int], list[LogRecord]]
package_logs_remove: Callable[[str, str | None], None]

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# pylint: disable=too-many-public-methods
import contextlib
from urllib.parse import quote_plus as url_encode
@ -166,13 +165,6 @@ class WebClient(Client, SyncAhrimanClient):
"""
return f"{self.address}/api/v1/status"
def configuration_reload(self) -> None:
"""
reload configuration
"""
with contextlib.suppress(Exception):
self.make_request("POST", f"{self.address}/api/v1/service/config")
def event_add(self, event: Event) -> None:
"""
create new event
@ -334,15 +326,12 @@ class WebClient(Client, SyncAhrimanClient):
self.make_request("POST", self._logs_url(log_record.log_record_id.package_base),
params=self.repository_id.query(), json=payload, suppress_errors=True)
def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
limit: int = -1, offset: int = 0) -> list[LogRecord]:
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
"""
get package logs
Args:
package_base(str): package base
version(str | None, optional): package version to search (Default value = None)
process_id(str | None, optional): process identifier to search (Default value = None)
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
offset(int, optional): records offset (Default value = 0)
@ -350,10 +339,6 @@ class WebClient(Client, SyncAhrimanClient):
list[LogRecord]: package logs
"""
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
if version is not None:
query.append(("version", version))
if process_id is not None:
query.append(("process_id", process_id))
with contextlib.suppress(Exception):
response = self.make_request("GET", self._logs_url(package_base), params=query)

View File

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

View File

@ -17,15 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import Any, Protocol
class Comparable(Protocol):
"""
class which supports :func:`__lt__` operation`
"""
def __lt__(self, other: Any) -> bool: ...
from typing import Protocol
class HasBool(Protocol):

View File

@ -51,7 +51,6 @@ __all__ = [
"parse_version",
"partition",
"pretty_datetime",
"pretty_interval",
"pretty_size",
"safe_filename",
"srcinfo_property",
@ -137,8 +136,7 @@ def check_output(*args: str, exception: Exception | Callable[[int, list[str], st
} | environment
with subprocess.Popen(args, cwd=cwd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
user=user, env=full_environment, text=True, encoding="utf8", errors="backslashreplace",
bufsize=1) as process:
user=user, env=full_environment, text=True, encoding="utf8", bufsize=1) as process:
if input_data is not None:
input_channel = get_io(process, "stdin")
input_channel.write(input_data)
@ -354,28 +352,6 @@ def pretty_datetime(timestamp: datetime.datetime | float | int | None) -> str:
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
def pretty_interval(interval: int) -> str:
"""
convert time interval to string
Args:
interval(int): time interval in seconds
Returns:
str: pretty printable interval as string
"""
minutes, seconds = divmod(interval, 60)
hours, minutes = divmod(minutes, 60)
return " ".join([
f"{value} {description}{"s" if value > 1 else ""}"
for value, description in [
(hours, "hour"),
(minutes, "minute"),
(seconds, "second"),
] if value > 0
])
def pretty_size(size: float | None, level: int = 0) -> str:
"""
convert size to string

View File

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

View File

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

View File

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

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import contextlib
import os
import shutil
@ -222,14 +221,22 @@ class RepositoryPaths(LazyLogging):
stat = path.stat()
return stat.st_uid, stat.st_gid
def _chown(self, path: Path) -> None:
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
Args:
package_base(str): package base name
Returns:
Path: full path to directory for specified package base cache
"""
return self.cache / package_base
def chown(self, path: Path) -> None:
"""
set owner of path recursively (from root) to root owner
Notes:
More likely you don't want to call this method explicitly, consider using :func:`preserve_owner`
as context manager instead
Args:
path(Path): path to be chown
@ -249,56 +256,6 @@ class RepositoryPaths(LazyLogging):
set_owner(path)
path = path.parent
def cache_for(self, package_base: str) -> Path:
"""
get path to cached PKGBUILD and package sources for the package base
Args:
package_base(str): package base name
Returns:
Path: full path to directory for specified package base cache
"""
return self.cache / package_base
@contextlib.contextmanager
def preserve_owner(self, path: Path | None = None) -> Generator[None, None, None]:
"""
perform any action preserving owner for any newly created file or directory
Args:
path(Path | None, optional): use this path as root instead of repository root (Default value = None)
Examples:
This method is designed to use as context manager when you are going to perform operations which might
change filesystem, especially if you are doing it under unsafe flag, e.g.::
>>> with paths.preserve_owner():
>>> paths.tree_create()
Note, however, that this method doesn't handle any exceptions and will eventually interrupt
if there will be any.
"""
path = path or self.root
def walk(root: Path) -> Generator[Path, None, None]:
# basically walk, but skipping some content
for child in root.iterdir():
yield child
if child in (self.chroot.parent,):
yield from child.iterdir() # we only yield top-level in chroot directory
elif child.is_dir():
yield from walk(child)
# get current filesystem and run action
previous_snapshot = set(walk(path))
yield
# get newly created files and directories and chown them
new_entries = set(walk(path)).difference(previous_snapshot)
for entry in new_entries:
self._chown(entry)
def tree_clear(self, package_base: str) -> None:
"""
clear package specific files
@ -317,13 +274,12 @@ class RepositoryPaths(LazyLogging):
"""
if self.repository_id.is_empty:
return # do not even try to create tree in case if no repository id set
with self.preserve_owner():
for directory in (
self.cache,
self.chroot,
self.packages,
self.pacman,
self.repository,
):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
for directory in (
self.cache,
self.chroot,
self.packages,
self.pacman,
self.repository,
):
directory.mkdir(mode=0o755, parents=True, exist_ok=True)
self.chown(directory)

View File

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

View File

@ -22,7 +22,6 @@ from ahriman.web.schemas.aur_package_schema import AURPackageSchema
from ahriman.web.schemas.auth_schema import AuthSchema
from ahriman.web.schemas.build_options_schema import BuildOptionsSchema
from ahriman.web.schemas.changes_schema import ChangesSchema
from ahriman.web.schemas.configuration_schema import ConfigurationSchema
from ahriman.web.schemas.counters_schema import CountersSchema
from ahriman.web.schemas.dependencies_schema import DependenciesSchema
from ahriman.web.schemas.error_schema import ErrorSchema
@ -35,7 +34,6 @@ from ahriman.web.schemas.log_schema import LogSchema
from ahriman.web.schemas.login_schema import LoginSchema
from ahriman.web.schemas.logs_rotate_schema import LogsRotateSchema
from ahriman.web.schemas.logs_schema import LogsSchema
from ahriman.web.schemas.logs_search_schema import LogsSearchSchema
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
from ahriman.web.schemas.package_name_schema import PackageNameSchema
from ahriman.web.schemas.package_names_schema import PackageNamesSchema

View File

@ -25,11 +25,11 @@ class AURPackageSchema(Schema):
response AUR package schema
"""
description = fields.String(required=True, metadata={
"description": "Package description",
"example": "ArcH linux ReposItory MANager",
})
package = fields.String(required=True, metadata={
"description": "Package base",
"example": "ahriman",
})
description = fields.String(required=True, metadata={
"description": "Package description",
"example": "ArcH linux ReposItory MANager",
})

View File

@ -25,10 +25,10 @@ class ChangesSchema(Schema):
response package changes schema
"""
changes = fields.String(metadata={
"description": "Package changes in patch format",
})
last_commit_sha = fields.String(metadata={
"description": "Last recorded commit hash",
"example": "f1875edca1eb8fc0e55c41d1cae5fa05b6b7c6",
})
changes = fields.String(metadata={
"description": "Package changes in patch format",
})

View File

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

View File

@ -25,6 +25,18 @@ class CountersSchema(Schema):
response package counters schema
"""
total = fields.Integer(required=True, metadata={
"description": "Total amount of packages",
"example": 6,
})
_unknown = fields.Integer(data_key="unknown", required=True, metadata={
"description": "Amount of packages in unknown state",
"example": 0,
})
pending = fields.Integer(required=True, metadata={
"description": "Amount of packages in pending state",
"example": 2,
})
building = fields.Integer(required=True, metadata={
"description": "Amount of packages in building state",
"example": 1,
@ -33,19 +45,7 @@ class CountersSchema(Schema):
"description": "Amount of packages in failed state",
"example": 1,
})
pending = fields.Integer(required=True, metadata={
"description": "Amount of packages in pending state",
"example": 2,
})
success = fields.Integer(required=True, metadata={
"description": "Amount of packages in success state",
"example": 3,
})
total = fields.Integer(required=True, metadata={
"description": "Total amount of packages",
"example": 6,
})
unknown_ = fields.Integer(data_key="unknown", required=True, metadata={
"description": "Amount of packages in unknown state",
"example": 0,
})

View File

@ -30,17 +30,17 @@ class EventSchema(Schema):
"description": "Event creation timestamp",
"example": 1680537091,
})
data = fields.Dict(keys=fields.String(), metadata={
"description": "Event metadata if available",
})
event = fields.String(required=True, metadata={
"description": "Event type",
"example": EventType.PackageUpdated,
})
message = fields.String(metadata={
"description": "Event message if available",
})
object_id = fields.String(required=True, metadata={
"description": "Event object identifier",
"example": "ahriman",
})
message = fields.String(metadata={
"description": "Event message if available",
})
data = fields.Dict(keys=fields.String(), metadata={
"description": "Event metadata if available",
})

View File

@ -31,14 +31,14 @@ class EventSearchSchema(PaginationSchema):
"description": "Event type",
"example": EventType.PackageUpdated,
})
from_date = fields.Integer(metadata={
"description": "Minimal creation timestamp, inclusive",
"example": 1680537091,
})
object_id = fields.String(metadata={
"description": "Event object identifier",
"example": "ahriman",
})
from_date = fields.Integer(metadata={
"description": "Minimal creation timestamp, inclusive",
"example": 1680537091,
})
to_date = fields.Integer(metadata={
"description": "Maximal creation timestamp, exclusive",
"example": 1680537091,

View File

@ -33,10 +33,10 @@ class LogSchema(Schema):
message = fields.String(required=True, metadata={
"description": "Log message",
})
process_id = fields.String(metadata={
"description": "Process unique identifier",
})
version = fields.String(required=True, metadata={
"description": "Package version to tag",
"example": __version__,
})
process_id = fields.String(metadata={
"description": "Process unique identifier",
})

View File

@ -25,11 +25,11 @@ class LoginSchema(Schema):
request login schema
"""
password = fields.String(required=True, metadata={
"description": "Login password",
"example": "pa55w0rd",
})
username = fields.String(required=True, metadata={
"description": "Login username",
"example": "user",
})
password = fields.String(required=True, metadata={
"description": "Login password",
"example": "pa55w0rd",
})

View File

@ -1,39 +0,0 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from ahriman import __version__
from ahriman.web.apispec import fields
from ahriman.web.schemas.pagination_schema import PaginationSchema
class LogsSearchSchema(PaginationSchema):
"""
request log search schema
"""
head = fields.Boolean(metadata={
"description": "Return versions only without fetching logs themselves",
})
process_id = fields.String(metadata={
"description": "Process unique identifier to search",
})
version = fields.String(metadata={
"description": "Package version to search",
"example": __version__,
})

View File

@ -37,14 +37,22 @@ class PackagePropertiesSchema(Schema):
"description": "Package build timestamp",
"example": 1680537091,
})
check_depends = fields.List(fields.String(), metadata={
"description": "Package test dependencies list",
"example": ["python-pytest"],
})
depends = fields.List(fields.String(), metadata={
"description": "Package dependencies list",
"example": ["devtools"],
})
make_depends = fields.List(fields.String(), metadata={
"description": "Package make dependencies list",
"example": ["python-build"],
})
opt_depends = fields.List(fields.String(), metadata={
"description": "Package optional dependencies list",
"example": ["python-aiohttp"],
})
check_depends = fields.List(fields.String(), metadata={
"description": "Package test dependencies list",
"example": ["python-pytest"],
})
description = fields.String(metadata={
"description": "Package description",
"example": "ArcH linux ReposItory MANager",
@ -65,14 +73,6 @@ class PackagePropertiesSchema(Schema):
"description": "Package licenses",
"example": ["GPL3"],
})
make_depends = fields.List(fields.String(), metadata={
"description": "Package make dependencies list",
"example": ["python-build"],
})
opt_depends = fields.List(fields.String(), metadata={
"description": "Package optional dependencies list",
"example": ["python-aiohttp"],
})
provides = fields.List(fields.String(), metadata={
"description": "Package provides list",
"example": ["ahriman-git"],

View File

@ -32,18 +32,18 @@ class PackageSchema(Schema):
"description": "Package base",
"example": "ahriman",
})
packager = fields.String(metadata={
"description": "packager for the last success package build",
"example": "ahriman bot <ahriman@example.com>",
version = fields.String(required=True, metadata={
"description": "Package version",
"example": __version__,
})
remote = fields.Nested(RemoteSchema(), required=True, metadata={
"description": "Package remote properties",
})
packages = fields.Dict(
keys=fields.String(), values=fields.Nested(PackagePropertiesSchema()), required=True, metadata={
"description": "Packages which belong to this base",
})
remote = fields.Nested(RemoteSchema(), required=True, metadata={
"description": "Package remote properties",
})
version = fields.String(required=True, metadata={
"description": "Package version",
"example": __version__,
packager = fields.String(metadata={
"description": "packager for the last success package build",
"example": "ahriman bot <ahriman@example.com>",
})

View File

@ -25,19 +25,19 @@ class RepositoryStatsSchema(Schema):
response repository stats schema
"""
archive_size = fields.Int(metadata={
"description": "Total archive size of the packages in bytes",
"example": 42000,
})
bases = fields.Int(metadata={
"description": "Amount of unique packages bases",
"example": 2,
})
installed_size = fields.Int(metadata={
"description": "Total installed size of the packages in bytes",
"example": 42000000,
})
packages = fields.Int(metadata={
"description": "Amount of unique packages",
"example": 4,
})
archive_size = fields.Int(metadata={
"description": "Total archive size of the packages in bytes",
"example": 42000,
})
installed_size = fields.Int(metadata={
"description": "Total installed size of the packages in bytes",
"example": 42000000,
})

View File

@ -25,7 +25,7 @@ class SearchSchema(Schema):
request package search schema
"""
for_ = fields.List(fields.String(), data_key="for", required=True, metadata={
_for = fields.List(fields.String(), data_key="for", required=True, metadata={
"description": "Keyword for search",
"example": ["ahriman"],
})

View File

@ -22,7 +22,6 @@ import aiohttp_jinja2
from typing import Any, ClassVar
from ahriman.core.auth.helpers import authorized_userid
from ahriman.core.utils import pretty_interval
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec import aiohttp_apispec
from ahriman.web.views.base import BaseView
@ -38,10 +37,6 @@ class IndexView(BaseView):
* control - HTML to insert for login control, HTML string, required
* enabled - whether authorization is enabled by configuration or not, boolean, required
* username - authenticated username if any, string, null means not authenticated
* autorefresh_intervals - auto refresh intervals, optional
* interval - auto refresh interval in milliseconds, integer, required
* is_active - is current interval active or not, boolean, required
* text - text representation of the interval (e.g. "30 seconds"), string, required
* docs_enabled - indicates if api docs is enabled, boolean, required
* index_url - url to the repository index, string, optional
* repositories - list of repositories unique identifiers, required
@ -71,19 +66,8 @@ class IndexView(BaseView):
"username": auth_username,
}
autorefresh_intervals = [
{
"interval": interval * 1000, # milliseconds
"is_active": index == 0, # first element is always default
"text": pretty_interval(interval),
}
for index, interval in enumerate(self.configuration.getintlist("web", "autorefresh_intervals", fallback=[]))
if interval > 0 # special case if 0 exists and first, refresh will not be turned on by default
]
return {
"auth": auth,
"autorefresh_intervals": sorted(autorefresh_intervals, key=lambda interval: interval["interval"]),
"docs_enabled": aiohttp_apispec is not None,
"index_url": self.configuration.get("web", "index_url", fallback=None),
"repositories": [

View File

@ -21,7 +21,6 @@ from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from collections.abc import Callable
from typing import ClassVar
from ahriman.core.types import Comparable
from ahriman.models.user_access import UserAccess
from ahriman.models.worker import Worker
from ahriman.web.apispec.decorators import apidocs
@ -75,7 +74,7 @@ class WorkersView(BaseView):
"""
workers = self.workers.workers
comparator: Callable[[Worker], Comparable] = lambda item: item.identifier
comparator: Callable[[Worker], str] = lambda item: item.identifier
response = [worker.view() for worker in sorted(workers, key=comparator)]
return json_response(response)

View File

@ -90,7 +90,7 @@ class LogsView(StatusViewGuard, BaseView):
try:
_, status = self.service().package_get(package_base)
logs = self.service(package_base=package_base).package_logs_get(package_base, None, None, -1, 0)
logs = self.service(package_base=package_base).package_logs_get(package_base, -1, 0)
except UnknownPackageError:
raise HTTPNotFound(reason=f"Package {package_base} is unknown")

View File

@ -23,7 +23,6 @@ from aiohttp.web import HTTPNoContent, Response, json_response
from collections.abc import Callable
from typing import ClassVar
from ahriman.core.types import Comparable
from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package
from ahriman.models.user_access import UserAccess
@ -69,7 +68,7 @@ class PackagesView(StatusViewGuard, BaseView):
repository_id = self.repository_id()
packages = self.service(repository_id).packages
comparator: Callable[[tuple[Package, BuildStatus]], Comparable] = lambda items: items[0].base
comparator: Callable[[tuple[Package, BuildStatus]], str] = lambda items: items[0].base
response = [
{
"package": package.view(),

View File

@ -1,84 +0,0 @@
#
# Copyright (c) 2021-2025 ahriman team.
#
# This file is part of ahriman
# (see https://github.com/arcan1s/ahriman).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPNoContent, Response, json_response
from typing import ClassVar
from ahriman.core.formatters import ConfigurationPrinter
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import ConfigurationSchema
from ahriman.web.views.base import BaseView
class ConfigView(BaseView):
"""
configuration control view
Attributes:
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
ROUTES = ["/api/v1/service/config"]
@apidocs(
tags=["Actions"],
summary="Get configuration",
description="Get current web service configuration as nested dictionary",
permission=GET_PERMISSION,
schema=ConfigurationSchema(many=True),
)
async def get(self) -> Response:
"""
get current web service configuration
Returns:
Response: current web service configuration as nested dictionary
"""
dump = self.configuration.dump()
response = [
{
"section": section,
"key": key,
"value": value,
} for section, values in dump.items()
for key, value in values.items()
if key not in ConfigurationPrinter.HIDE_KEYS
]
return json_response(response)
@apidocs(
tags=["Actions"],
summary="Reload configuration",
description="Reload configuration from current files",
permission=POST_PERMISSION,
)
async def post(self) -> None:
"""
reload web service configuration
Raises:
HTTPNoContent: on success response
"""
self.configuration.reload()
raise HTTPNoContent

View File

@ -22,7 +22,6 @@ from collections.abc import Callable
from typing import ClassVar
from ahriman.core.alpm.remote import AUR
from ahriman.core.types import Comparable
from ahriman.models.aur_package import AURPackage
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
@ -71,12 +70,7 @@ class SearchView(BaseView):
if not packages:
raise HTTPNotFound(reason=f"No packages found for terms: {search}")
comparator: Callable[[AURPackage], Comparable] = \
lambda item: (
item.package_base not in search, # inverted because False < True
not any(item.package_base.startswith(term) for term in search), # same as above
item.package_base,
)
comparator: Callable[[AURPackage], str] = lambda item: str(item.package_base)
response = [
{
"package": package.package_base,

View File

@ -17,22 +17,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import itertools
from aiohttp.web import Response, json_response
from dataclasses import replace
from typing import ClassVar
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import LogSchema, LogsSearchSchema, PackageNameSchema
from ahriman.web.schemas import LogSchema, PackageNameSchema, PaginationSchema
from ahriman.web.views.base import BaseView
from ahriman.web.views.status_view_guard import StatusViewGuard
class LogsView(StatusViewGuard, BaseView):
""" else:
"""
package logs web view
Attributes:
@ -51,7 +47,7 @@ class LogsView(StatusViewGuard, BaseView):
error_404_description="Package base and/or repository are unknown",
schema=LogSchema(many=True),
match_schema=PackageNameSchema,
query_schema=LogsSearchSchema,
query_schema=PaginationSchema,
)
async def get(self) -> Response:
"""
@ -65,19 +61,8 @@ class LogsView(StatusViewGuard, BaseView):
"""
package_base = self.request.match_info["package"]
limit, offset = self.page()
version = self.request.query.get("version", None)
process = self.request.query.get("process_id", None)
logs = self.service(package_base=package_base).package_logs_get(package_base, version, process, limit, offset)
head = self.request.query.get("head", "false")
# pylint: disable=protected-access
if self.configuration._convert_to_boolean(head): # type: ignore[attr-defined]
# logs should be sorted already
logs = [
replace(next(log_records), message="") # remove messages
for _, log_records in itertools.groupby(logs, lambda log_record: log_record.log_record_id)
]
logs = self.service(package_base=package_base).package_logs_get(package_base, limit, offset)
response = [log_record.view() for log_record in logs]
return json_response(response)

View File

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

View File

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

View File

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

@ -141,7 +141,7 @@ def test_add_remote_missing(application_packages: ApplicationPackages, mocker: M
"""
must raise UnknownPackageError if remote package wasn't found
"""
mocker.patch("requests.get", side_effect=Exception)
mocker.patch("requests.get", side_effect=Exception())
with pytest.raises(UnknownPackageError):
application_packages._add_remote("url")

View File

@ -135,7 +135,7 @@ def test_unknown_no_aur(application_repository: ApplicationRepository, package_a
must return empty list in case if there is locally stored PKGBUILD
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception)
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception())
mocker.patch("ahriman.models.package.Package.from_build", return_value=package_ahriman)
mocker.patch("pathlib.Path.is_dir", return_value=True)
mocker.patch("ahriman.core.build_tools.sources.Sources.has_remotes", return_value=False)
@ -149,7 +149,7 @@ def test_unknown_no_aur_no_local(application_repository: ApplicationRepository,
must return list of packages missing in aur and in local storage
"""
mocker.patch("ahriman.core.repository.repository.Repository.packages", return_value=[package_ahriman])
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception)
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=Exception())
mocker.patch("pathlib.Path.is_dir", return_value=False)
packages = application_repository.unknown()

View File

@ -46,7 +46,7 @@ def test_call_exception(args: argparse.Namespace, configuration: Configuration,
"""
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=Exception)
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=Exception())
logging_mock = mocker.patch("logging.Logger.exception")
_, repository_id = configuration.check_loaded()
@ -60,7 +60,7 @@ def test_call_exit_code(args: argparse.Namespace, configuration: Configuration,
"""
args.configuration = Path("")
args.quiet = False
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=ExitCode)
mocker.patch("ahriman.core.configuration.Configuration.from_path", side_effect=ExitCode())
logging_mock = mocker.patch("logging.Logger.exception")
_, repository_id = configuration.check_loaded()
@ -144,7 +144,6 @@ def test_repositories_extract(args: argparse.Namespace, configuration: Configura
args.architecture = "arch"
args.configuration = configuration.path
args.repository = "repo"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
@ -160,7 +159,6 @@ def test_repositories_extract_repository(args: argparse.Namespace, configuration
"""
args.architecture = "arch"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value={"repo"})
@ -177,7 +175,6 @@ def test_repositories_extract_repository_legacy(args: argparse.Namespace, config
"""
args.architecture = "arch"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())
@ -194,7 +191,6 @@ def test_repositories_extract_architecture(args: argparse.Namespace, configurati
"""
args.configuration = configuration.path
args.repository = "repo"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures",
return_value={"arch"})
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
@ -211,7 +207,6 @@ def test_repositories_extract_empty(args: argparse.Namespace, configuration: Con
"""
args.command = "config"
args.configuration = configuration.path
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures", return_value=set())
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories", return_value=set())
@ -226,7 +221,6 @@ def test_repositories_extract_systemd(args: argparse.Namespace, configuration: C
"""
args.configuration = configuration.path
args.repository_id = "i686/some/repo/name"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
@ -242,7 +236,6 @@ def test_repositories_extract_systemd_with_dash(args: argparse.Namespace, config
"""
args.configuration = configuration.path
args.repository_id = "i686-some-repo-name"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
@ -258,7 +251,6 @@ def test_repositories_extract_systemd_legacy(args: argparse.Namespace, configura
"""
args.configuration = configuration.path
args.repository_id = "i686"
mocker.patch("ahriman.core.configuration.Configuration.load", new=lambda self, _: self.copy_from(configuration))
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
return_value=set())

View File

@ -79,7 +79,6 @@ def test_run_empty_exception(args: argparse.Namespace, configuration: Configurat
args = _default_args(args)
args.exit_code = True
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
mocker.patch("ahriman.core.repository.Repository.packages", return_value=[])
mocker.patch("ahriman.application.application.Application.update")
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")

View File

@ -1,27 +0,0 @@
import argparse
from pytest_mock import MockerFixture
from ahriman.application.handlers.reload import Reload
from ahriman.core.configuration import Configuration
from ahriman.core.repository import Repository
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
mocker: MockerFixture) -> None:
"""
must run command
"""
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
run_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.configuration_reload")
_, repository_id = configuration.check_loaded()
Reload.run(args, repository_id, configuration, report=False)
run_mock.assert_called_once_with()
def test_disallow_multi_architecture_run() -> None:
"""
must not allow multi architecture run
"""
assert not Reload.ALLOW_MULTI_ARCHITECTURE_RUN

View File

@ -58,11 +58,9 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
sudo_configuration_mock = mocker.patch("ahriman.application.handlers.setup.Setup.configuration_create_sudo")
executable_mock = mocker.patch("ahriman.application.handlers.setup.Setup.executable_create")
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
_, repository_id = configuration.check_loaded()
Setup.run(args, repository_id, configuration, report=False)
owner_guard_mock.assert_called_once_with()
ahriman_configuration_mock.assert_called_once_with(args, repository_id, configuration)
devtools_configuration_mock.assert_called_once_with(
repository_id, args.from_configuration, args.mirror, args.multilib, f"file://{repository_paths.repository}")
@ -270,11 +268,13 @@ def test_executable_create(configuration: Configuration, repository_paths: Repos
"""
must create executable
"""
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
unlink_mock = mocker.patch("pathlib.Path.unlink")
_, repository_id = configuration.check_loaded()
Setup.executable_create(repository_paths, repository_id)
chown_mock.assert_called_once_with(Setup.build_command(repository_paths.root, repository_id))
symlink_mock.assert_called_once_with(Setup.ARCHBUILD_COMMAND_PATH)
unlink_mock.assert_called_once_with(missing_ok=True)

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