mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-05-04 20:23:49 +00:00
Compare commits
11 Commits
3d50ea4686
...
0e7ca45b54
Author | SHA1 | Date | |
---|---|---|---|
0e7ca45b54 | |||
1ec1bd05eb | |||
6d05389639 | |||
daf9841717 | |||
0d243a781a | |||
cf2e66a934 | |||
f01f35238d | |||
d30d512eb6 | |||
0437f90e5a | |||
3cab65855a | |||
ecfb615f97 |
9
.github/workflows/setup.sh
vendored
9
.github/workflows/setup.sh
vendored
@ -10,9 +10,9 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
|
||||
# refresh the image
|
||||
pacman -Syu --noconfirm
|
||||
# main dependencies
|
||||
pacman -Sy --noconfirm devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-systemd sudo
|
||||
pacman -Sy --noconfirm devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo python-systemd sudo
|
||||
# make dependencies
|
||||
pacman -Sy --noconfirm python-build python-flit python-installer python-tox python-wheel
|
||||
pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
||||
# optional dependencies
|
||||
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||
# VCS support
|
||||
@ -36,6 +36,9 @@ sudo -u nobody -- makepkg --packagelist | grep -v -- -debug- | pacman -U --nocon
|
||||
# create machine-id which is required by build tools
|
||||
systemd-machine-id-setup
|
||||
|
||||
# remove unused dependencies
|
||||
pacman -Qdtq | pacman -Rscn --noconfirm -
|
||||
|
||||
# initial setup command as root
|
||||
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
|
||||
ahriman -a x86_64 -r "github" service-setup --packager "ahriman bot <ahriman@example.com>" "${WEB_ARGS[@]}"
|
||||
@ -50,7 +53,7 @@ if [[ -z $MINIMAL_INSTALL ]]; then
|
||||
WEB_PID=$!
|
||||
fi
|
||||
# add the first package
|
||||
sudo -u ahriman -- ahriman package-add --now ahriman
|
||||
sudo -u ahriman -- ahriman --log-handler console package-add --now ahriman
|
||||
# check if package was actually installed
|
||||
test -n "$(find "/var/lib/ahriman/repository/github/x86_64" -name "ahriman*pkg*")"
|
||||
# run package check
|
||||
|
@ -3,7 +3,7 @@ version: 2
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
python: "3.12"
|
||||
|
||||
python:
|
||||
install:
|
||||
|
@ -32,10 +32,10 @@ RUN useradd -m -d "/home/build" -s "/usr/bin/nologin" build && \
|
||||
COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
|
||||
## install package dependencies
|
||||
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
|
||||
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo && \
|
||||
pacman -Sy --noconfirm --asdeps python-build python-flit python-installer python-wheel && \
|
||||
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-pyelftools python-requests python-srcinfo && \
|
||||
pacman -Sy --noconfirm --asdeps base-devel python-build python-flit python-installer python-wheel && \
|
||||
pacman -Sy --noconfirm --asdeps breezy git mercurial python-aiohttp python-boto3 python-cryptography python-jinja python-requests-unixsocket python-systemd rsync subversion && \
|
||||
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-cors \
|
||||
runuser -u build -- install-aur-package python-aioauth-client python-webargs python-aiohttp-apispec-git python-aiohttp-cors \
|
||||
python-aiohttp-jinja2 python-aiohttp-session python-aiohttp-security
|
||||
|
||||
## FIXME since 1.0.4 devtools requires dbus to be run, which doesn't work now in container
|
||||
|
@ -6,7 +6,7 @@ for PACKAGE in "$@"; do
|
||||
BUILD_DIR="$(mktemp -d)"
|
||||
git clone https://aur.archlinux.org/"$PACKAGE".git "$BUILD_DIR"
|
||||
cd "$BUILD_DIR"
|
||||
makepkg --noconfirm --install --rmdeps --syncdeps
|
||||
makepkg --nocheck --noconfirm --install --rmdeps --syncdeps
|
||||
cd /
|
||||
rm -r "$BUILD_DIR"
|
||||
done
|
||||
|
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@ -20,6 +20,14 @@ ahriman.core.alpm.pacman module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.alpm.pacman\_database module
|
||||
-----------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.alpm.pacman_database
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.alpm.repo module
|
||||
-----------------------------
|
||||
|
||||
|
@ -108,6 +108,14 @@ ahriman.core.database.migrations.m012\_last\_commit\_sha module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.database.migrations.m013\_dependencies module
|
||||
----------------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.database.migrations.m013_dependencies
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
@ -28,6 +28,14 @@ ahriman.core.database.operations.changes\_operations module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.database.operations.dependencies\_operations module
|
||||
----------------------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.database.operations.dependencies_operations
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.database.operations.logs\_operations module
|
||||
--------------------------------------------------------
|
||||
|
||||
|
@ -60,6 +60,14 @@ ahriman.models.counters module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.dependencies module
|
||||
----------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.dependencies
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.internal\_status module
|
||||
--------------------------------------
|
||||
|
||||
@ -108,6 +116,14 @@ ahriman.models.package module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.package\_archive module
|
||||
--------------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.package_archive
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.package\_description module
|
||||
------------------------------------------
|
||||
|
||||
|
@ -53,6 +53,7 @@ libalpm and AUR related configuration. Group name can refer to architecture, e.g
|
||||
* ``mirror`` - package database mirror used by pacman for synchronization, string, required. This option supports standard pacman substitutions with ``$arch`` and ``$repo``. Note that the mentioned mirror should contain all repositories which are set by ``alpm.repositories`` option.
|
||||
* ``repositories`` - list of pacman repositories, used for package search, space separated list of strings, required.
|
||||
* ``root`` - root for alpm library, string, required. In the most cases it must point to the system root.
|
||||
* ``sync_files_database`` - download files database from mirror, boolean, required.
|
||||
* ``use_ahriman_cache`` - use local pacman package cache instead of system one, boolean, required. With this option enabled you might want to refresh database periodically (available as additional flag for some subcommands). If set to ``no``, databases must be synchronized manually.
|
||||
|
||||
``auth`` group
|
||||
@ -81,6 +82,7 @@ Build related configuration. Group name can refer to architecture, e.g. ``build:
|
||||
* ``archbuild_flags`` - additional flags passed to ``archbuild`` command, space separated list of strings, optional.
|
||||
* ``build_command`` - default build command, string, required.
|
||||
* ``ignore_packages`` - list packages to ignore during a regular update (manual update will still work), space separated list of strings, optional.
|
||||
* ``include_debug_packages`` - distribute debug packages, boolean, optional, default ``yes``.
|
||||
* ``makepkg_flags`` - additional flags passed to ``makepkg`` command, space separated list of strings, optional.
|
||||
* ``makechrootpkg_flags`` - additional flags passed to ``makechrootpkg`` command, space separated list of strings, optional.
|
||||
* ``triggers`` - list of ``ahriman.core.triggers.Trigger`` class implementation (e.g. ``ahriman.core.report.ReportTrigger ahriman.core.upload.UploadTrigger``) which will be loaded and run at the end of processing, space separated list of strings, optional. You can also specify triggers by their paths, e.g. ``/usr/lib/python3.10/site-packages/ahriman/core/report/report.py.ReportTrigger``. Triggers are run in the order of definition.
|
||||
|
@ -1,13 +1,13 @@
|
||||
# Maintainer: Evgeniy Alekseev
|
||||
|
||||
pkgname='ahriman'
|
||||
pkgver=2.13.4
|
||||
pkgver=2.13.6
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
url="https://github.com/arcan1s/ahriman"
|
||||
license=('GPL3')
|
||||
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
|
||||
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-pyelftools' 'python-requests' 'python-srcinfo')
|
||||
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
|
||||
optdepends=('breezy: -bzr packages support'
|
||||
'darcs: -darcs packages support'
|
||||
|
@ -17,6 +17,8 @@ mirror = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
repositories = core extra multilib
|
||||
; Pacman's root directory. In the most cases it must point to the system root.
|
||||
root = /
|
||||
; Sync files databases too, which is required by deep dependencies check
|
||||
sync_files_database = yes
|
||||
; Use local packages cache. If this option is enabled, the service will be able to synchronize databases (available
|
||||
; as additional option for some subcommands). If set to no, databases must be synchronized manually.
|
||||
use_ahriman_cache = yes
|
||||
@ -46,8 +48,12 @@ allow_read_only = yes
|
||||
[build]
|
||||
; List of additional flags passed to archbuild command.
|
||||
;archbuild_flags =
|
||||
; Path to build command
|
||||
;build_command =
|
||||
; List of packages to be ignored during automatic updates.
|
||||
;ignore_packages =
|
||||
; Include debug packages
|
||||
;include_debug_packages = yes
|
||||
; List of additional flags passed to makechrootpkg command.
|
||||
;makechrootpkg_flags =
|
||||
; List of additional flags passed to makepkg command.
|
||||
|
@ -38,10 +38,6 @@
|
||||
<script>
|
||||
const keyImportModal = $("#key-import-modal");
|
||||
const keyImportForm = $("#key-import-form");
|
||||
keyImportModal.on("hidden.bs.modal", () => {
|
||||
keyImportBodyInput.text("");
|
||||
keyImportForm.trigger("reset");
|
||||
});
|
||||
|
||||
const keyImportBodyInput = $("#key-import-body-input");
|
||||
const keyImportCopyButton = $("#key-import-copy-button");
|
||||
@ -90,4 +86,11 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(() => {
|
||||
keyImportModal.on("hidden.bs.modal", () => {
|
||||
keyImportBodyInput.text("");
|
||||
keyImportForm.trigger("reset");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -36,9 +36,6 @@
|
||||
<script>
|
||||
const loginModal = $("#login-modal");
|
||||
const loginForm = $("#login-form");
|
||||
loginModal.on("hidden.bs.modal", () => {
|
||||
loginForm.trigger("reset");
|
||||
});
|
||||
|
||||
const loginPasswordInput = $("#login-password");
|
||||
const loginUsernameInput = $("#login-username");
|
||||
@ -77,4 +74,10 @@
|
||||
showHidePasswordButton.addClass("bi-eye");
|
||||
}
|
||||
}
|
||||
|
||||
$(() => {
|
||||
loginModal.on("hidden.bs.modal", () => {
|
||||
loginForm.trigger("reset");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -43,41 +43,10 @@
|
||||
<script>
|
||||
const packageAddModal = $("#package-add-modal");
|
||||
const packageAddForm = $("#package-add-form");
|
||||
packageAddModal.on("shown.bs.modal", () => {
|
||||
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
||||
});
|
||||
packageAddModal.on("hidden.bs.modal", () => {
|
||||
packageAddVariablesDiv.empty();
|
||||
packageAddForm.trigger("reset");
|
||||
});
|
||||
|
||||
const packageAddInput = $("#package-add-input");
|
||||
const packageAddRepositoryInput = $("#package-add-repository-input");
|
||||
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist");
|
||||
packageAddInput.keyup(() => {
|
||||
clearTimeout(packageAddInput.data("timeout"));
|
||||
packageAddInput.data("timeout", setTimeout($.proxy(() => {
|
||||
const value = packageAddInput.val();
|
||||
|
||||
if (value.length >= 3) {
|
||||
$.ajax({
|
||||
url: "/api/v1/service/search",
|
||||
data: {"for": value},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const options = response.map(pkg => {
|
||||
const option = document.createElement("option");
|
||||
option.value = pkg.package;
|
||||
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||
return option;
|
||||
});
|
||||
packageAddKnownPackagesList.empty().append(options);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, this), 500));
|
||||
});
|
||||
|
||||
const packageAddVariablesDiv = $("#package-add-variables-div");
|
||||
|
||||
@ -156,4 +125,39 @@
|
||||
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
|
||||
}
|
||||
}
|
||||
|
||||
$(() => {
|
||||
packageAddModal.on("shown.bs.modal", () => {
|
||||
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
||||
});
|
||||
packageAddModal.on("hidden.bs.modal", () => {
|
||||
packageAddVariablesDiv.empty();
|
||||
packageAddForm.trigger("reset");
|
||||
});
|
||||
|
||||
packageAddInput.keyup(() => {
|
||||
clearTimeout(packageAddInput.data("timeout"));
|
||||
packageAddInput.data("timeout", setTimeout($.proxy(() => {
|
||||
const value = packageAddInput.val();
|
||||
|
||||
if (value.length >= 3) {
|
||||
$.ajax({
|
||||
url: "/api/v1/service/search",
|
||||
data: {"for": value},
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
success: response => {
|
||||
const options = response.map(pkg => {
|
||||
const option = document.createElement("option");
|
||||
option.value = pkg.package;
|
||||
option.innerText = `${pkg.package} (${pkg.description})`;
|
||||
return option;
|
||||
});
|
||||
packageAddKnownPackagesList.empty().append(options);
|
||||
},
|
||||
});
|
||||
}
|
||||
}, this), 500));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -72,26 +72,6 @@
|
||||
const packageInfoModal = $("#package-info-modal");
|
||||
const packageInfoModalHeader = $("#package-info-modal-header");
|
||||
const packageInfo = $("#package-info");
|
||||
packageInfoModal.on("hidden.bs.modal", () => {
|
||||
packageInfoAurUrl.empty();
|
||||
packageInfoDepends.empty();
|
||||
packageInfoGroups.empty();
|
||||
packageInfoLicenses.empty();
|
||||
packageInfoPackager.empty();
|
||||
packageInfoPackages.empty();
|
||||
packageInfoUpstreamUrl.empty();
|
||||
packageInfoVersion.empty();
|
||||
|
||||
packageInfoVariablesBlock.attr("hidden", true);
|
||||
packageInfoVariablesDiv.empty();
|
||||
|
||||
packageInfoLogsInput.empty();
|
||||
packageInfoChangesInput.empty();
|
||||
|
||||
packageInfoModal.trigger("reset");
|
||||
|
||||
hideInfoControls(true);
|
||||
});
|
||||
|
||||
const packageInfoLogsInput = $("#package-info-logs-input");
|
||||
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
|
||||
@ -309,4 +289,27 @@
|
||||
|
||||
if (isPackageBaseSet) packageInfoModal.modal("show");
|
||||
}
|
||||
|
||||
$(() => {
|
||||
packageInfoModal.on("hidden.bs.modal", () => {
|
||||
packageInfoAurUrl.empty();
|
||||
packageInfoDepends.empty();
|
||||
packageInfoGroups.empty();
|
||||
packageInfoLicenses.empty();
|
||||
packageInfoPackager.empty();
|
||||
packageInfoPackages.empty();
|
||||
packageInfoUpstreamUrl.empty();
|
||||
packageInfoVersion.empty();
|
||||
|
||||
packageInfoVariablesBlock.attr("hidden", true);
|
||||
packageInfoVariablesDiv.empty();
|
||||
|
||||
packageInfoLogsInput.empty();
|
||||
packageInfoChangesInput.empty();
|
||||
|
||||
packageInfoModal.trigger("reset");
|
||||
|
||||
hideInfoControls(true);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -35,11 +35,6 @@
|
||||
<script>
|
||||
const packageRebuildModal = $("#package-rebuild-modal");
|
||||
const packageRebuildForm = $("#package-rebuild-form");
|
||||
packageRebuildModal.on("shown.bs.modal", () => {
|
||||
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
||||
|
||||
});
|
||||
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
|
||||
|
||||
const packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
|
||||
const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
|
||||
@ -54,4 +49,12 @@
|
||||
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
|
||||
}
|
||||
}
|
||||
|
||||
$(() => {
|
||||
packageRebuildModal.on("shown.bs.modal", () => {
|
||||
$(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
|
||||
|
||||
});
|
||||
packageRebuildModal.on("hidden.bs.modal", () => { packageRebuildForm.trigger("reset"); });
|
||||
});
|
||||
</script>
|
||||
|
@ -9,46 +9,8 @@
|
||||
const packageInfoUpdateButton = $("#package-info-update-button");
|
||||
|
||||
let repository = null;
|
||||
$("#repositories a").on("click", (event) => {
|
||||
const element = event.target;
|
||||
repository = {
|
||||
architecture: element.dataset.architecture,
|
||||
repository: element.dataset.repository,
|
||||
};
|
||||
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
|
||||
$(`#${element.id}`).tab("show");
|
||||
reload();
|
||||
});
|
||||
|
||||
const table = $("#packages");
|
||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
|
||||
packageRemoveButton.prop("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", () => {
|
||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
||||
pickerInput.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
cancelLabel: "Clear",
|
||||
},
|
||||
});
|
||||
|
||||
pickerInput.on("apply.daterangepicker", (event, picker) => {
|
||||
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
|
||||
pickerInput.on("cancel.daterangepicker", () => {
|
||||
pickerInput.val("");
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
});
|
||||
|
||||
const statusBadge = $("#badge-status");
|
||||
const versionBadge = $("#badge-version");
|
||||
@ -221,8 +183,47 @@
|
||||
}
|
||||
|
||||
$(() => {
|
||||
table.bootstrapTable({});
|
||||
$("#repositories a").on("click", event => {
|
||||
const element = event.target;
|
||||
repository = {
|
||||
architecture: element.dataset.architecture,
|
||||
repository: element.dataset.repository,
|
||||
};
|
||||
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`);
|
||||
$(`#${element.id}`).tab("show");
|
||||
reload();
|
||||
});
|
||||
|
||||
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", () => {
|
||||
packageRemoveButton.prop("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", () => {
|
||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
||||
pickerInput.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
cancelLabel: "Clear",
|
||||
},
|
||||
});
|
||||
|
||||
pickerInput.on("apply.daterangepicker", (event, picker) => {
|
||||
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
|
||||
pickerInput.on("cancel.daterangepicker", () => {
|
||||
pickerInput.val("");
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
});
|
||||
|
||||
statusBadge.popover();
|
||||
selectRepository();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@ -102,25 +102,6 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
||||
|
||||
<script>
|
||||
const table = $("#packages");
|
||||
table.on("created-controls.bs.table", () => {
|
||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
||||
pickerInput.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
cancelLabel: "Clear",
|
||||
},
|
||||
});
|
||||
|
||||
pickerInput.on("apply.daterangepicker", (event, picker) => {
|
||||
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
|
||||
pickerInput.on("cancel.daterangepicker", () => {
|
||||
pickerInput.val("");
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
});
|
||||
|
||||
const pacmanConf = $("#pacman-conf");
|
||||
const pacmanConfCopyButton = $("#copy-btn");
|
||||
@ -141,6 +122,28 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
|
||||
function filterListLicenses() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||
}
|
||||
|
||||
$(() => {
|
||||
table.on("created-controls.bs.table", () => {
|
||||
const pickerInput = $(".bootstrap-table-filter-control-timestamp");
|
||||
pickerInput.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
locale: {
|
||||
cancelLabel: "Clear",
|
||||
},
|
||||
});
|
||||
|
||||
pickerInput.on("apply.daterangepicker", (event, picker) => {
|
||||
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
|
||||
pickerInput.on("cancel.daterangepicker", () => {
|
||||
pickerInput.val("");
|
||||
table.bootstrapTable("triggerSearch");
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" integrity="sha384-1H217gwSVyLSIfaLxHbE7dRb3v4mYCKbpQvzx0cegeju1MVsGrX5xXxAvs/HgeFs" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-8hHkOkbWN1TLWwet/jpbJ0zbx3FJDeYJgQ8dX1mRrv/vfCfHCqFSFZYCgaMML3z9" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-u4eJN1VWrTf/FnYYQJo2kqJyVxEQf5UmWY4iUcNAoLenOEtEuCkfwc5bKvZOWBi5" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" integrity="sha384-4ZTAzTbfB8H7hkWtXbyNDzDvxirmBT7EmURIvfOJ3Foympc+OD9p+bZNNENaJXgW" crossorigin="anonymous" type="application/javascript"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" integrity="sha384-Gn1XZMJEKL3ycoWq97jYAl+FP3vXQYE2ObBgzgcPMKOZdUZdF6ZuyUxbGC2bAnUT" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.28.0/tableExport.min.js" integrity="sha384-1Rz4Kz/y1rSWw+ZsjTcxB684XgofbO8iizY+UFIzCwFeQ+QUyhBNWBMh/STOyomI" crossorigin="anonymous" type="application/javascript"></script>
|
||||
|
||||
|
@ -27,12 +27,12 @@ _shtab_ahriman_patch_list_option_strings=('-h' '--help' '-e' '--exit-code' '-v'
|
||||
_shtab_ahriman_patch_remove_option_strings=('-h' '--help' '-v' '--variable')
|
||||
_shtab_ahriman_patch_set_add_option_strings=('-h' '--help' '-t' '--track')
|
||||
_shtab_ahriman_repo_backup_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_check_option_strings=('-h' '--help' '--changes' '--no-changes' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_check_option_strings=('-h' '--help' '--changes' '--no-changes' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_repo_check_option_strings=('-h' '--help' '--changes' '--no-changes' '--check-files' '--no-check-files' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_check_option_strings=('-h' '--help' '--changes' '--no-changes' '--check-files' '--no-check-files' '-e' '--exit-code' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '--partitions' '--no-partitions' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
|
||||
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
|
||||
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
|
||||
@ -47,8 +47,8 @@ _shtab_ahriman_repo_sync_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_sync_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions')
|
||||
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
|
||||
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--changes' '--no-changes' '--check-files' '--no-check-files' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
|
||||
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
|
||||
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
|
||||
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
|
||||
@ -243,6 +243,8 @@ _shtab_ahriman_repo_check__h_nargs=0
|
||||
_shtab_ahriman_repo_check___help_nargs=0
|
||||
_shtab_ahriman_repo_check___changes_nargs=0
|
||||
_shtab_ahriman_repo_check___no_changes_nargs=0
|
||||
_shtab_ahriman_repo_check___check_files_nargs=0
|
||||
_shtab_ahriman_repo_check___no_check_files_nargs=0
|
||||
_shtab_ahriman_repo_check__e_nargs=0
|
||||
_shtab_ahriman_repo_check___exit_code_nargs=0
|
||||
_shtab_ahriman_repo_check___vcs_nargs=0
|
||||
@ -254,6 +256,8 @@ _shtab_ahriman_check__h_nargs=0
|
||||
_shtab_ahriman_check___help_nargs=0
|
||||
_shtab_ahriman_check___changes_nargs=0
|
||||
_shtab_ahriman_check___no_changes_nargs=0
|
||||
_shtab_ahriman_check___check_files_nargs=0
|
||||
_shtab_ahriman_check___no_check_files_nargs=0
|
||||
_shtab_ahriman_check__e_nargs=0
|
||||
_shtab_ahriman_check___exit_code_nargs=0
|
||||
_shtab_ahriman_check___vcs_nargs=0
|
||||
@ -270,6 +274,8 @@ _shtab_ahriman_repo_daemon___aur_nargs=0
|
||||
_shtab_ahriman_repo_daemon___no_aur_nargs=0
|
||||
_shtab_ahriman_repo_daemon___changes_nargs=0
|
||||
_shtab_ahriman_repo_daemon___no_changes_nargs=0
|
||||
_shtab_ahriman_repo_daemon___check_files_nargs=0
|
||||
_shtab_ahriman_repo_daemon___no_check_files_nargs=0
|
||||
_shtab_ahriman_repo_daemon___dependencies_nargs=0
|
||||
_shtab_ahriman_repo_daemon___no_dependencies_nargs=0
|
||||
_shtab_ahriman_repo_daemon___dry_run_nargs=0
|
||||
@ -291,6 +297,8 @@ _shtab_ahriman_daemon___aur_nargs=0
|
||||
_shtab_ahriman_daemon___no_aur_nargs=0
|
||||
_shtab_ahriman_daemon___changes_nargs=0
|
||||
_shtab_ahriman_daemon___no_changes_nargs=0
|
||||
_shtab_ahriman_daemon___check_files_nargs=0
|
||||
_shtab_ahriman_daemon___no_check_files_nargs=0
|
||||
_shtab_ahriman_daemon___dependencies_nargs=0
|
||||
_shtab_ahriman_daemon___no_dependencies_nargs=0
|
||||
_shtab_ahriman_daemon___dry_run_nargs=0
|
||||
@ -358,6 +366,8 @@ _shtab_ahriman_repo_update___aur_nargs=0
|
||||
_shtab_ahriman_repo_update___no_aur_nargs=0
|
||||
_shtab_ahriman_repo_update___changes_nargs=0
|
||||
_shtab_ahriman_repo_update___no_changes_nargs=0
|
||||
_shtab_ahriman_repo_update___check_files_nargs=0
|
||||
_shtab_ahriman_repo_update___no_check_files_nargs=0
|
||||
_shtab_ahriman_repo_update___dependencies_nargs=0
|
||||
_shtab_ahriman_repo_update___no_dependencies_nargs=0
|
||||
_shtab_ahriman_repo_update___dry_run_nargs=0
|
||||
@ -380,6 +390,8 @@ _shtab_ahriman_update___aur_nargs=0
|
||||
_shtab_ahriman_update___no_aur_nargs=0
|
||||
_shtab_ahriman_update___changes_nargs=0
|
||||
_shtab_ahriman_update___no_changes_nargs=0
|
||||
_shtab_ahriman_update___check_files_nargs=0
|
||||
_shtab_ahriman_update___no_check_files_nargs=0
|
||||
_shtab_ahriman_update___dependencies_nargs=0
|
||||
_shtab_ahriman_update___no_dependencies_nargs=0
|
||||
_shtab_ahriman_update___dry_run_nargs=0
|
||||
@ -584,7 +596,7 @@ _set_new_action() {
|
||||
current_action_nargs=1
|
||||
fi
|
||||
|
||||
current_action_args_start_index=$(( $word_index + 1 ))
|
||||
current_action_args_start_index=$(( $word_index + 1 - $pos_only ))
|
||||
|
||||
current_action_is_positional=$2
|
||||
}
|
||||
@ -611,6 +623,7 @@ _shtab_ahriman() {
|
||||
|
||||
local prefix=_shtab_ahriman
|
||||
local word_index=0
|
||||
local pos_only=0 # "--" delimeter not encountered yet
|
||||
_set_parser_defaults
|
||||
word_index=1
|
||||
|
||||
@ -619,26 +632,30 @@ _shtab_ahriman() {
|
||||
while [ $word_index -ne $COMP_CWORD ]; do
|
||||
local this_word="${COMP_WORDS[$word_index]}"
|
||||
|
||||
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then
|
||||
# valid subcommand: add it to the prefix & reset the current action
|
||||
prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
|
||||
_set_parser_defaults
|
||||
fi
|
||||
if [[ $pos_only = 1 || " $this_word " != " -- " ]]; then
|
||||
if [[ -n $sub_parsers && " ${sub_parsers[@]} " == *" ${this_word} "* ]]; then
|
||||
# valid subcommand: add it to the prefix & reset the current action
|
||||
prefix="${prefix}_$(_shtab_replace_nonword $this_word)"
|
||||
_set_parser_defaults
|
||||
fi
|
||||
|
||||
if [[ " ${current_option_strings[@]} " == *" ${this_word} "* ]]; then
|
||||
# a new action should be acquired (due to recognised option string or
|
||||
# no more input expected from current action);
|
||||
# the next positional action can fill in here
|
||||
_set_new_action $this_word false
|
||||
fi
|
||||
if [[ " ${current_option_strings[@]} " == *" ${this_word} "* ]]; then
|
||||
# a new action should be acquired (due to recognised option string or
|
||||
# no more input expected from current action);
|
||||
# the next positional action can fill in here
|
||||
_set_new_action $this_word false
|
||||
fi
|
||||
|
||||
if [[ "$current_action_nargs" != "*" ]] && \
|
||||
[[ "$current_action_nargs" != "+" ]] && \
|
||||
[[ "$current_action_nargs" != *"..." ]] && \
|
||||
(( $word_index + 1 - $current_action_args_start_index >= \
|
||||
$current_action_nargs )); then
|
||||
$current_action_is_positional && let "completed_positional_actions += 1"
|
||||
_set_new_action "pos_${completed_positional_actions}" true
|
||||
if [[ "$current_action_nargs" != "*" ]] && \
|
||||
[[ "$current_action_nargs" != "+" ]] && \
|
||||
[[ "$current_action_nargs" != *"..." ]] && \
|
||||
(( $word_index + 1 - $current_action_args_start_index - $pos_only >= \
|
||||
$current_action_nargs )); then
|
||||
$current_action_is_positional && let "completed_positional_actions += 1"
|
||||
_set_new_action "pos_${completed_positional_actions}" true
|
||||
fi
|
||||
else
|
||||
pos_only=1 # "--" delimeter encountered
|
||||
fi
|
||||
|
||||
let "word_index+=1"
|
||||
@ -646,7 +663,7 @@ _shtab_ahriman() {
|
||||
|
||||
# Generate the completions
|
||||
|
||||
if [[ "${completing_word}" == -* ]]; then
|
||||
if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
|
||||
# optional argument started: use option strings
|
||||
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
|
||||
else
|
||||
|
@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2024\-02\-09" "ahriman" "Generated Python Manual"
|
||||
.TH AHRIMAN "1" "2024\-05\-05" "ahriman" "Generated Python Manual"
|
||||
.SH NAME
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
@ -447,7 +447,9 @@ backup repository settings and database
|
||||
path of the output archive
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-check'\/\fR
|
||||
usage: ahriman repo\-check [\-h] [\-\-changes | \-\-no\-changes] [\-e] [\-\-vcs | \-\-no\-vcs] [\-y] [package ...]
|
||||
usage: ahriman repo\-check [\-h] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files] [\-e] [\-\-vcs | \-\-no\-vcs]
|
||||
[\-y]
|
||||
[package ...]
|
||||
|
||||
check for packages updates. Same as repo\-update \-\-dry\-run \-\-no\-manual
|
||||
|
||||
@ -460,6 +462,10 @@ filter check by package base
|
||||
\fB\-\-changes\fR, \fB\-\-no\-changes\fR
|
||||
calculate changes from the latest known commit if available. Only applicable in dry run mode
|
||||
|
||||
.TP
|
||||
\fB\-\-check\-files\fR, \fB\-\-no\-check\-files\fR
|
||||
enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories)
|
||||
|
||||
.TP
|
||||
\fB\-e\fR, \fB\-\-exit\-code\fR
|
||||
return non\-zero exit status if result is empty
|
||||
@ -484,9 +490,9 @@ create package which contains list of available mirrors as set by configuration.
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-daemon'\/\fR
|
||||
usage: ahriman repo\-daemon [\-h] [\-i INTERVAL] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes]
|
||||
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-\-increment | \-\-no\-increment]
|
||||
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-\-partitions | \-\-no\-partitions]
|
||||
[\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
|
||||
[\-\-check\-files | \-\-no\-check\-files] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run]
|
||||
[\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual]
|
||||
[\-\-partitions | \-\-no\-partitions] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
|
||||
|
||||
start process which periodically will run update process
|
||||
|
||||
@ -503,6 +509,10 @@ enable or disable checking for AUR updates
|
||||
\fB\-\-changes\fR, \fB\-\-no\-changes\fR
|
||||
calculate changes from the latest known commit if available. Only applicable in dry run mode
|
||||
|
||||
.TP
|
||||
\fB\-\-check\-files\fR, \fB\-\-no\-check\-files\fR
|
||||
enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories)
|
||||
|
||||
.TP
|
||||
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
|
||||
process missing package dependencies
|
||||
@ -649,9 +659,9 @@ run triggers on empty build result as configured by settings
|
||||
instead of running all triggers as set by configuration, just process specified ones in order of mention
|
||||
|
||||
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR
|
||||
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-dependencies | \-\-no\-dependencies]
|
||||
[\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local]
|
||||
[\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
|
||||
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-changes | \-\-no\-changes] [\-\-check\-files | \-\-no\-check\-files]
|
||||
[\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e] [\-\-increment | \-\-no\-increment]
|
||||
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
|
||||
[package ...]
|
||||
|
||||
check for packages updates and run build process if requested
|
||||
@ -669,6 +679,10 @@ enable or disable checking for AUR updates
|
||||
\fB\-\-changes\fR, \fB\-\-no\-changes\fR
|
||||
calculate changes from the latest known commit if available. Only applicable in dry run mode
|
||||
|
||||
.TP
|
||||
\fB\-\-check\-files\fR, \fB\-\-no\-check\-files\fR
|
||||
enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories)
|
||||
|
||||
.TP
|
||||
\fB\-\-dependencies\fR, \fB\-\-no\-dependencies\fR
|
||||
process missing package dependencies
|
||||
|
@ -120,6 +120,7 @@ _shtab_ahriman_aur_search_options=(
|
||||
_shtab_ahriman_check_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
|
||||
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: True)]:vcs:"
|
||||
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
|
||||
@ -153,6 +154,7 @@ _shtab_ahriman_daemon_options=(
|
||||
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
|
||||
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
|
||||
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
|
||||
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
|
||||
"--dry-run[just perform check for updates, same as check command (default\: False)]"
|
||||
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
|
||||
@ -322,6 +324,7 @@ _shtab_ahriman_repo_backup_options=(
|
||||
_shtab_ahriman_repo_check_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
|
||||
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
{--vcs,--no-vcs}"[fetch actual version of VCS packages (default\: True)]:vcs:"
|
||||
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
|
||||
@ -363,6 +366,7 @@ _shtab_ahriman_repo_daemon_options=(
|
||||
{-i,--interval}"[interval between runs in seconds (default\: 43200)]:interval:"
|
||||
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
|
||||
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
|
||||
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
|
||||
"--dry-run[just perform check for updates, same as check command (default\: False)]"
|
||||
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
|
||||
@ -460,6 +464,7 @@ _shtab_ahriman_repo_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
|
||||
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
|
||||
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
|
||||
"--dry-run[just perform check for updates, same as check command (default\: False)]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@ -601,6 +606,7 @@ _shtab_ahriman_update_options=(
|
||||
"(- : *)"{-h,--help}"[show this help message and exit]"
|
||||
{--aur,--no-aur}"[enable or disable checking for AUR updates (default\: True)]:aur:"
|
||||
{--changes,--no-changes}"[calculate changes from the latest known commit if available. Only applicable in dry run mode (default\: True)]:changes:"
|
||||
{--check-files,--no-check-files}"[enable or disable checking of broken dependencies (e.g. dynamically linked libraries or modules directories) (default\: True)]:check_files:"
|
||||
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
|
||||
"--dry-run[just perform check for updates, same as check command (default\: False)]"
|
||||
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
|
||||
@ -736,4 +742,12 @@ _shtab_ahriman() {
|
||||
|
||||
|
||||
typeset -A opt_args
|
||||
_shtab_ahriman "$@"
|
||||
|
||||
if [[ $zsh_eval_context[-1] == eval ]]; then
|
||||
# eval/source/. command, register function for later
|
||||
compdef _shtab_ahriman -N ahriman
|
||||
else
|
||||
# autoload from fpath, call function directly
|
||||
_shtab_ahriman "$@"
|
||||
fi
|
||||
|
||||
|
@ -20,6 +20,7 @@ dependencies = [
|
||||
"cerberus",
|
||||
"inflection",
|
||||
"passlib",
|
||||
"pyelftools",
|
||||
"requests",
|
||||
"srcinfo",
|
||||
]
|
||||
@ -81,6 +82,7 @@ web = [
|
||||
"aiohttp_security",
|
||||
"cryptography",
|
||||
"requests-unixsocket", # required by unix socket support
|
||||
"setuptools", # required by aiohttp-apispec
|
||||
]
|
||||
|
||||
[tool.flit.sdist]
|
||||
|
@ -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.13.4"
|
||||
__version__ = "2.13.6"
|
||||
|
@ -537,6 +537,9 @@ def _set_repo_check_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||
"Only applicable in dry run mode",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||
"(e.g. dynamically linked libraries or modules directories)",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
|
||||
parser.add_argument("--vcs", help="fetch actual version of VCS packages",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
@ -605,6 +608,9 @@ def _set_repo_daemon_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||
"Only applicable in dry run mode",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||
"(e.g. dynamically linked libraries or modules directories)",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
|
||||
@ -826,6 +832,9 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
parser.add_argument("--changes", help="calculate changes from the latest known commit if available. "
|
||||
"Only applicable in dry run mode",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--check-files", help="enable or disable checking of broken dependencies "
|
||||
"(e.g. dynamically linked libraries or modules directories)",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dependencies", help="process missing package dependencies",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
|
||||
|
@ -62,10 +62,13 @@ class Application(ApplicationPackages, ApplicationRepository):
|
||||
"""
|
||||
known_packages: set[str] = set()
|
||||
# local set
|
||||
# this action is not really needed in case if ``alpm.use_ahriman_cache`` set to yes, because pacman
|
||||
# will eventually contain all the local packages
|
||||
for base in self.repository.packages():
|
||||
for package, properties in base.packages.items():
|
||||
known_packages.add(package)
|
||||
known_packages.update(properties.provides)
|
||||
# known pacman databases
|
||||
known_packages.update(self.repository.pacman.packages())
|
||||
return known_packages
|
||||
|
||||
|
@ -21,6 +21,7 @@ from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
@ -63,3 +64,13 @@ class ApplicationProperties(LazyLogging):
|
||||
str: repository architecture
|
||||
"""
|
||||
return self.repository_id.architecture
|
||||
|
||||
@property
|
||||
def reporter(self) -> Client:
|
||||
"""
|
||||
instance of the web/database client
|
||||
|
||||
Returns:
|
||||
Client: repository reposter
|
||||
"""
|
||||
return self.repository.reporter
|
||||
|
@ -91,10 +91,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
packages(Iterable[str]): only sign specified packages
|
||||
"""
|
||||
# copy to prebuilt directory
|
||||
for package in self.repository.packages():
|
||||
# no one requested this package
|
||||
if packages and package.base not in packages:
|
||||
continue
|
||||
for package in self.repository.packages(packages):
|
||||
for archive in package.packages.values():
|
||||
if archive.filepath is None:
|
||||
self.logger.warning("filepath is empty for %s", package.base)
|
||||
@ -179,7 +176,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
return result
|
||||
|
||||
def updates(self, filter_packages: Iterable[str], *,
|
||||
aur: bool, local: bool, manual: bool, vcs: bool) -> list[Package]:
|
||||
aur: bool, local: bool, manual: bool, vcs: bool, check_files: bool) -> list[Package]:
|
||||
"""
|
||||
get list of packages to run update process
|
||||
|
||||
@ -189,6 +186,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
local(bool): enable or disable checking of local packages for updates
|
||||
manual(bool): include or exclude manual updates
|
||||
vcs(bool): enable or disable checking of VCS packages
|
||||
check_files(bool): check for broken dependencies
|
||||
|
||||
Returns:
|
||||
list[Package]: list of out-of-dated packages
|
||||
@ -201,5 +199,7 @@ class ApplicationRepository(ApplicationProperties):
|
||||
updates.update({package.base: package for package in self.repository.updates_local(vcs=vcs)})
|
||||
if manual:
|
||||
updates.update({package.base: package for package in self.repository.updates_manual()})
|
||||
if check_files:
|
||||
updates.update({package.base: package for package in self.repository.updates_dependencies(filter_packages)})
|
||||
|
||||
return [package for _, package in sorted(updates.items())]
|
||||
|
@ -55,7 +55,7 @@ class Add(Handler):
|
||||
if not args.now:
|
||||
return
|
||||
|
||||
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False)
|
||||
packages = application.updates(args.package, aur=False, local=False, manual=True, vcs=False, check_files=False)
|
||||
packages = application.with_dependencies(packages, process_dependencies=args.dependencies)
|
||||
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
|
||||
|
||||
|
@ -18,10 +18,10 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
import pwd
|
||||
import tarfile
|
||||
|
||||
from pathlib import Path
|
||||
from tarfile import TarFile
|
||||
from pwd import getpwuid
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -49,7 +49,7 @@ class Backup(Handler):
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
backup_paths = Backup.get_paths(configuration)
|
||||
with TarFile(args.path, mode="w") as archive: # well we don't actually use compression
|
||||
with tarfile.open(args.path, mode="w") as archive: # well we don't actually use compression
|
||||
for backup_path in backup_paths:
|
||||
archive.add(backup_path)
|
||||
|
||||
@ -77,7 +77,7 @@ class Backup(Handler):
|
||||
|
||||
# gnupg home with imported keys
|
||||
uid, _ = repository_paths.root_owner
|
||||
system_user = pwd.getpwuid(uid)
|
||||
system_user = getpwuid(uid)
|
||||
gnupg_home = Path(system_user.pw_dir) / ".gnupg"
|
||||
if gnupg_home.is_dir():
|
||||
paths.add(gnupg_home)
|
||||
|
@ -18,8 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from tarfile import TarFile
|
||||
import tarfile
|
||||
|
||||
from ahriman.application.handlers.handler import Handler
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -45,5 +44,5 @@ class Restore(Handler):
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
with TarFile(args.path) as archive:
|
||||
archive.extractall(path=args.output)
|
||||
with tarfile.open(args.path) as archive:
|
||||
archive.extractall(path=args.output) # nosec
|
||||
|
@ -56,7 +56,7 @@ class StatusUpdate(Handler):
|
||||
if (local := next((package for package in packages if package.base == base), None)) is not None:
|
||||
client.package_add(local, args.status)
|
||||
else:
|
||||
client.package_update(base, args.status)
|
||||
client.package_set(base, args.status)
|
||||
case Action.Update:
|
||||
# update service status
|
||||
client.status_update(args.status)
|
||||
|
@ -48,7 +48,8 @@ class Update(Handler):
|
||||
application = Application(repository_id, configuration, report=report, refresh_pacman_database=args.refresh)
|
||||
application.on_start()
|
||||
|
||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs)
|
||||
packages = application.updates(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs,
|
||||
check_files=args.check_files)
|
||||
if args.dry_run: # some check specific actions
|
||||
if args.changes: # generate changes if requested
|
||||
application.changes(packages)
|
||||
|
@ -18,24 +18,31 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import shutil
|
||||
import tarfile
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from collections.abc import Generator, Iterable
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from pyalpm import DB, Handle, Package, SIG_PACKAGE, error as PyalpmError # type: ignore[import-not-found]
|
||||
from pyalpm import DB, Handle, Package, SIG_DATABASE_OPTIONAL, SIG_PACKAGE_OPTIONAL # type: ignore[import-not-found]
|
||||
from string import Template
|
||||
|
||||
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.util import trim_package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Pacman(LazyLogging):
|
||||
"""
|
||||
alpm wrapper
|
||||
|
||||
Attributes:
|
||||
configuration(Configuration): configuration instance
|
||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
repository_path(RepositoryPaths): repository paths instance
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
@ -48,8 +55,11 @@ class Pacman(LazyLogging):
|
||||
configuration(Configuration): configuration instance
|
||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||
"""
|
||||
self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle(
|
||||
repository_id, configuration, refresh_database=refresh_database)
|
||||
self.configuration = configuration
|
||||
self.repository_id = repository_id
|
||||
self.repository_paths = configuration.repository_paths
|
||||
|
||||
self.refresh_database = refresh_database
|
||||
|
||||
@cached_property
|
||||
def handle(self) -> Handle:
|
||||
@ -59,40 +69,39 @@ class Pacman(LazyLogging):
|
||||
Returns:
|
||||
Handle: generated pyalpm handle instance
|
||||
"""
|
||||
return self.__create_handle_fn()
|
||||
return self.__create_handle(refresh_database=self.refresh_database)
|
||||
|
||||
def __create_handle(self, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
refresh_database: PacmanSynchronization) -> Handle:
|
||||
def __create_handle(self, *, refresh_database: PacmanSynchronization) -> Handle:
|
||||
"""
|
||||
create lazy handle function
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
refresh_database(PacmanSynchronization): synchronize local cache to remote
|
||||
|
||||
Returns:
|
||||
Handle: fully initialized pacman handle
|
||||
"""
|
||||
root = configuration.getpath("alpm", "root")
|
||||
pacman_root = configuration.getpath("alpm", "database")
|
||||
use_ahriman_cache = configuration.getboolean("alpm", "use_ahriman_cache")
|
||||
mirror = configuration.get("alpm", "mirror")
|
||||
paths = configuration.repository_paths
|
||||
database_path = paths.pacman if use_ahriman_cache else pacman_root
|
||||
pacman_root = self.configuration.getpath("alpm", "database")
|
||||
use_ahriman_cache = self.configuration.getboolean("alpm", "use_ahriman_cache")
|
||||
|
||||
database_path = self.repository_paths.pacman if use_ahriman_cache else pacman_root
|
||||
root = self.configuration.getpath("alpm", "root")
|
||||
handle = Handle(str(root), str(database_path))
|
||||
for repository in configuration.getlist("alpm", "repositories"):
|
||||
database = self.database_init(handle, repository, mirror, repository_id.architecture)
|
||||
self.database_copy(handle, database, pacman_root, paths, use_ahriman_cache=use_ahriman_cache)
|
||||
|
||||
for repository in self.configuration.getlist("alpm", "repositories"):
|
||||
database = self.database_init(handle, repository, self.repository_id.architecture)
|
||||
self.database_copy(handle, database, pacman_root, use_ahriman_cache=use_ahriman_cache)
|
||||
|
||||
# install repository database too
|
||||
local_database = self.database_init(handle, self.repository_id.name, self.repository_id.architecture)
|
||||
self.database_copy(handle, local_database, pacman_root, use_ahriman_cache=use_ahriman_cache)
|
||||
|
||||
if use_ahriman_cache and refresh_database:
|
||||
self.database_sync(handle, force=refresh_database == PacmanSynchronization.Force)
|
||||
|
||||
return handle
|
||||
|
||||
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, paths: RepositoryPaths, *,
|
||||
use_ahriman_cache: bool) -> None:
|
||||
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, *, use_ahriman_cache: bool) -> None:
|
||||
"""
|
||||
copy database from the operating system root to the ahriman local home
|
||||
|
||||
@ -100,7 +109,6 @@ class Pacman(LazyLogging):
|
||||
handle(Handle): pacman handle which will be used for database copying
|
||||
database(DB): pacman database instance to be copied
|
||||
pacman_root(Path): operating system pacman root
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
use_ahriman_cache(bool): use local ahriman cache instead of system one
|
||||
"""
|
||||
def repository_database(root: Path) -> Path:
|
||||
@ -122,30 +130,36 @@ class Pacman(LazyLogging):
|
||||
return # database for some reason deos not exist
|
||||
self.logger.info("copy pacman database from operating system root to ahriman's home")
|
||||
shutil.copy(src, dst)
|
||||
paths.chown(dst)
|
||||
self.repository_paths.chown(dst)
|
||||
|
||||
def database_init(self, handle: Handle, repository: str, mirror: str, architecture: str) -> DB:
|
||||
def database_init(self, handle: Handle, repository: str, architecture: str) -> DB:
|
||||
"""
|
||||
create database instance from pacman handler and set its properties
|
||||
|
||||
Args:
|
||||
handle(Handle): pacman handle which will be used for database initializing
|
||||
repository(str): pacman repository name (e.g. core)
|
||||
mirror(str): arch linux mirror url
|
||||
architecture(str): repository architecture
|
||||
|
||||
Returns:
|
||||
DB: loaded pacman database instance
|
||||
"""
|
||||
self.logger.info("loading pacman database %s", repository)
|
||||
database: DB = handle.register_syncdb(repository, SIG_PACKAGE)
|
||||
database: DB = handle.register_syncdb(repository, SIG_DATABASE_OPTIONAL | SIG_PACKAGE_OPTIONAL)
|
||||
|
||||
# replace variables in mirror address
|
||||
variables = {
|
||||
"arch": architecture,
|
||||
"repo": repository,
|
||||
}
|
||||
database.servers = [Template(mirror).safe_substitute(variables)]
|
||||
if repository != self.repository_id.name:
|
||||
mirror = self.configuration.get("alpm", "mirror")
|
||||
# replace variables in mirror address
|
||||
variables = {
|
||||
"arch": architecture,
|
||||
"repo": repository,
|
||||
}
|
||||
server = Template(mirror).safe_substitute(variables)
|
||||
else:
|
||||
# special case, same database, use local storage instead
|
||||
server = f"file://{self.repository_paths.repository}"
|
||||
|
||||
database.servers = [server]
|
||||
|
||||
return database
|
||||
|
||||
@ -160,13 +174,44 @@ class Pacman(LazyLogging):
|
||||
self.logger.info("refresh ahriman's home pacman database (force refresh %s)", force)
|
||||
transaction = handle.init_transaction()
|
||||
for database in handle.get_syncdbs():
|
||||
try:
|
||||
database.update(force)
|
||||
except PyalpmError:
|
||||
self.logger.exception("exception during update %s", database.name)
|
||||
PacmanDatabase(database, self.configuration).sync(force=force)
|
||||
transaction.release()
|
||||
|
||||
def package_get(self, package_name: str) -> Generator[Package, None, None]:
|
||||
def files(self, packages: Iterable[str] | None = None) -> dict[str, set[Path]]:
|
||||
"""
|
||||
extract list of known packages from the databases
|
||||
|
||||
Args:
|
||||
packages(Iterable[str] | None, optional): filter by package names (Default value = None)
|
||||
|
||||
Returns:
|
||||
dict[str, set[Path]]: map of package name to its list of files
|
||||
"""
|
||||
packages = packages or []
|
||||
|
||||
def extract(tar: tarfile.TarFile) -> Generator[tuple[str, set[Path]], None, None]:
|
||||
for descriptor in filter(lambda info: info.path.endswith("/files"), tar.getmembers()):
|
||||
package, *_ = str(Path(descriptor.path).parent).rsplit("-", 2)
|
||||
if packages and package not in packages:
|
||||
continue # skip unused packages
|
||||
content = tar.extractfile(descriptor)
|
||||
if content is None:
|
||||
continue
|
||||
files = {Path(filename.decode("utf8").rstrip()) for filename in content.readlines()}
|
||||
|
||||
yield package, files
|
||||
|
||||
result: dict[str, set[Path]] = {}
|
||||
for database in self.handle.get_syncdbs():
|
||||
database_file = self.repository_paths.pacman / "sync" / f"{database.name}.files.tar.gz"
|
||||
if not database_file.is_file():
|
||||
continue # no database file found
|
||||
with tarfile.open(database_file, "r:gz") as archive:
|
||||
result.update(extract(archive))
|
||||
|
||||
return result
|
||||
|
||||
def package(self, package_name: str) -> Generator[Package, None, None]:
|
||||
"""
|
||||
retrieve list of the packages from the repository by name
|
||||
|
||||
|
170
src/ahriman/core/alpm/pacman_database.py
Normal file
170
src/ahriman/core/alpm/pacman_database.py
Normal file
@ -0,0 +1,170 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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 os
|
||||
import shutil
|
||||
|
||||
from email.utils import parsedate_to_datetime
|
||||
from pathlib import Path
|
||||
from pyalpm import DB # type: ignore[import-not-found]
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import PacmanError
|
||||
from ahriman.core.http import SyncHttpClient
|
||||
|
||||
|
||||
class PacmanDatabase(SyncHttpClient):
|
||||
"""
|
||||
implementation for database sync, because pyalpm is not always enough
|
||||
|
||||
Attributes:
|
||||
LAST_MODIFIED_HEADER(str): last modified header name
|
||||
database(DB): pyalpm database object
|
||||
repository_paths(RepositoryPaths): repository paths instance
|
||||
sync_files_database(bool): sync files database
|
||||
"""
|
||||
|
||||
LAST_MODIFIED_HEADER = "Last-Modified"
|
||||
|
||||
def __init__(self, database: DB, configuration: Configuration) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
database(DB): pyalpm database object
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
SyncHttpClient.__init__(self)
|
||||
self.timeout = None # reset timeout
|
||||
|
||||
self.database = database
|
||||
self.repository_paths = configuration.repository_paths
|
||||
|
||||
self.sync_files_database = configuration.getboolean("alpm", "sync_files_database")
|
||||
|
||||
def copy(self, remote_path: Path, local_path: Path) -> None:
|
||||
"""
|
||||
copy local database file
|
||||
|
||||
Args:
|
||||
remote_path(Path): path to source (remote) file
|
||||
local_path(Path): path to locally stored file
|
||||
"""
|
||||
shutil.copy(remote_path, local_path)
|
||||
|
||||
def download(self, url: str, local_path: Path) -> None:
|
||||
"""
|
||||
download remote file and store it to local path with the correct last modified headers
|
||||
|
||||
Args:
|
||||
url(str): remote url to request file
|
||||
local_path(Path): path to locally stored file
|
||||
|
||||
Raises:
|
||||
PacmanError: in case if no last-modified header was found
|
||||
"""
|
||||
response = self.make_request("GET", url, stream=True)
|
||||
if self.LAST_MODIFIED_HEADER not in response.headers:
|
||||
raise PacmanError("No last-modified header found")
|
||||
|
||||
with local_path.open("wb") as local_file:
|
||||
for chunk in response.iter_content(chunk_size=1024):
|
||||
local_file.write(chunk)
|
||||
|
||||
# set correct (a,m)time for the file
|
||||
remote_changed = parsedate_to_datetime(response.headers[self.LAST_MODIFIED_HEADER]).timestamp()
|
||||
os.utime(local_path, (remote_changed, remote_changed))
|
||||
|
||||
def is_outdated(self, url: str, local_path: Path) -> bool:
|
||||
"""
|
||||
check if local file is outdated
|
||||
|
||||
Args:
|
||||
url(str): remote url to request last modified header
|
||||
local_path(Path): path to locally stored file
|
||||
|
||||
Returns:
|
||||
bool: True in case if remote file is newer than local file
|
||||
|
||||
Raises:
|
||||
PacmanError: in case if no last-modified header was found
|
||||
"""
|
||||
if not local_path.is_file():
|
||||
return True # no local file found, requires to update
|
||||
|
||||
response = self.make_request("HEAD", url)
|
||||
if self.LAST_MODIFIED_HEADER not in response.headers:
|
||||
raise PacmanError("No last-modified header found")
|
||||
|
||||
remote_changed = parsedate_to_datetime(response.headers["Last-Modified"]).timestamp()
|
||||
local_changed = local_path.stat().st_mtime
|
||||
|
||||
return remote_changed > local_changed
|
||||
|
||||
def sync(self, *, force: bool) -> None:
|
||||
"""
|
||||
sync packages and files databases
|
||||
|
||||
Args:
|
||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||
"""
|
||||
try:
|
||||
self.sync_packages(force=force)
|
||||
if self.sync_files_database:
|
||||
self.sync_files(force=force)
|
||||
except Exception:
|
||||
self.logger.exception("exception during update %s", self.database.name)
|
||||
|
||||
def sync_files(self, *, force: bool) -> None:
|
||||
"""
|
||||
sync files by using http request
|
||||
|
||||
Args:
|
||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||
"""
|
||||
server = next(iter(self.database.servers))
|
||||
filename = f"{self.database.name}.files.tar.gz"
|
||||
url = f"{server}/{filename}"
|
||||
|
||||
remote_uri = urlparse(url)
|
||||
local_path = Path(self.repository_paths.pacman / "sync" / filename)
|
||||
|
||||
match remote_uri.scheme:
|
||||
case "http" | "https":
|
||||
if not force and not self.is_outdated(url, local_path):
|
||||
return
|
||||
|
||||
self.download(url, local_path)
|
||||
|
||||
case "file":
|
||||
# just copy file as it is relatively cheap operation, no need to check timestamps
|
||||
self.copy(Path(remote_uri.path), local_path)
|
||||
|
||||
case other:
|
||||
raise PacmanError(f"Unknown or unsupported URL scheme {other}")
|
||||
|
||||
def sync_packages(self, *, force: bool) -> None:
|
||||
"""
|
||||
sync packages by using built-in pyalpm methods
|
||||
|
||||
Args:
|
||||
force(bool): force database synchronization (same as ``pacman -Syy``)
|
||||
"""
|
||||
self.database.update(force)
|
@ -56,6 +56,6 @@ class OfficialSyncdb(Official):
|
||||
raise UnknownPackageError(package_name)
|
||||
|
||||
try:
|
||||
return next(AURPackage.from_pacman(package) for package in pacman.package_get(package_name))
|
||||
return next(AURPackage.from_pacman(package) for package in pacman.package(package_name))
|
||||
except StopIteration:
|
||||
raise UnknownPackageError(package_name) from None
|
||||
|
@ -68,7 +68,7 @@ class Repo(LazyLogging):
|
||||
path(Path): path to archive to add
|
||||
"""
|
||||
check_output(
|
||||
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
|
||||
"repo-add", *self.sign_args, "--remove", str(self.repo_path), str(path),
|
||||
exception=BuildError.from_process(path.name),
|
||||
cwd=self.paths.repository,
|
||||
logger=self.logger,
|
||||
@ -78,8 +78,13 @@ class Repo(LazyLogging):
|
||||
"""
|
||||
create empty repository database. It just calls add with empty arguments
|
||||
"""
|
||||
check_output("repo-add", *self.sign_args, str(self.repo_path),
|
||||
cwd=self.paths.repository, logger=self.logger, user=self.uid)
|
||||
# since pacman-6.1.0 repo-add doesn't create empty database in case if no packages supplied
|
||||
# this code creates empty files instead
|
||||
if self.repo_path.exists():
|
||||
return # database is already created, skip this part
|
||||
|
||||
self.repo_path.touch(exist_ok=True)
|
||||
(self.paths.repository / f"{self.name}.db").symlink_to(self.repo_path)
|
||||
|
||||
def remove(self, package: str, filename: Path) -> None:
|
||||
"""
|
||||
|
@ -69,7 +69,8 @@ class OAuth(Mapping):
|
||||
Returns:
|
||||
str: login control as html code to insert
|
||||
"""
|
||||
return f"""<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-{self.icon}"></i> login</a>"""
|
||||
return f"""<a class="nav-link" href="/api/v1/login" title="login via OAuth2"><i class="bi bi-{
|
||||
self.icon}"></i> login</a>"""
|
||||
|
||||
@staticmethod
|
||||
def get_provider(name: str) -> type[aioauth_client.OAuth2Client]:
|
||||
|
@ -38,6 +38,7 @@ class Task(LazyLogging):
|
||||
archbuild_flags(list[str]): command flags for archbuild command
|
||||
architecture(str): repository architecture
|
||||
build_command(str): build command
|
||||
include_debug_packages(bool): whether to include debug packages or not
|
||||
makechrootpkg_flags(list[str]): command flags for makechrootpkg command
|
||||
makepkg_flags(list[str]): command flags for makepkg command
|
||||
package(Package): package definitions
|
||||
@ -63,6 +64,7 @@ class Task(LazyLogging):
|
||||
|
||||
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
|
||||
self.build_command = configuration.get("build", "build_command")
|
||||
self.include_debug_packages = configuration.getboolean("build", "include_debug_packages", fallback=True)
|
||||
self.makepkg_flags = configuration.getlist("build", "makepkg_flags", fallback=[])
|
||||
self.makechrootpkg_flags = configuration.getlist("build", "makechrootpkg_flags", fallback=[])
|
||||
|
||||
@ -99,9 +101,11 @@ class Task(LazyLogging):
|
||||
environment=environment,
|
||||
)
|
||||
|
||||
# well it is not actually correct, but we can deal with it
|
||||
package_list_command = ["makepkg", "--packagelist"]
|
||||
if not self.include_debug_packages:
|
||||
package_list_command.append("OPTIONS=(!debug)") # disable debug flag manually
|
||||
packages = check_output(
|
||||
"makepkg", "--packagelist",
|
||||
*package_list_command,
|
||||
exception=BuildError.from_process(self.package.base),
|
||||
cwd=sources_dir,
|
||||
logger=self.logger,
|
||||
|
@ -89,6 +89,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"path_exists": True,
|
||||
"path_type": "dir",
|
||||
},
|
||||
"sync_files_database": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
"required": True,
|
||||
},
|
||||
"use_ahriman_cache": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
@ -176,6 +181,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"empty": False,
|
||||
},
|
||||
},
|
||||
"include_debug_packages": {
|
||||
"type": "boolean",
|
||||
"coerce": "boolean",
|
||||
},
|
||||
"makepkg_flags": {
|
||||
"type": "list",
|
||||
"coerce": "list",
|
||||
|
32
src/ahriman/core/database/migrations/m013_dependencies.py
Normal file
32
src/ahriman/core/database/migrations/m013_dependencies.py
Normal file
@ -0,0 +1,32 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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/>.
|
||||
#
|
||||
__all__ = ["steps"]
|
||||
|
||||
|
||||
steps = [
|
||||
"""
|
||||
create table package_dependencies (
|
||||
package_base text not null,
|
||||
repository text not null,
|
||||
dependencies json not null,
|
||||
unique (package_base, repository)
|
||||
)
|
||||
""",
|
||||
]
|
@ -20,6 +20,7 @@
|
||||
from ahriman.core.database.operations.auth_operations import AuthOperations
|
||||
from ahriman.core.database.operations.build_operations import BuildOperations
|
||||
from ahriman.core.database.operations.changes_operations import ChangesOperations
|
||||
from ahriman.core.database.operations.dependencies_operations import DependenciesOperations
|
||||
from ahriman.core.database.operations.logs_operations import LogsOperations
|
||||
from ahriman.core.database.operations.package_operations import PackageOperations
|
||||
from ahriman.core.database.operations.patch_operations import PatchOperations
|
||||
|
@ -64,7 +64,7 @@ class ChangesOperations(Operations):
|
||||
|
||||
def changes_insert(self, package_base: str, changes: Changes, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
insert packages to build queue
|
||||
insert package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to insert
|
||||
|
124
src/ahriman/core/database/operations/dependencies_operations.py
Normal file
124
src/ahriman/core/database/operations/dependencies_operations.py
Normal file
@ -0,0 +1,124 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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 pathlib import Path
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations.operations import Operations
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class DependenciesOperations(Operations):
|
||||
"""
|
||||
operations for dependencies table
|
||||
"""
|
||||
|
||||
def dependencies_get(self, package_base: str | None = None,
|
||||
repository_id: RepositoryId | None = None) -> list[Dependencies]:
|
||||
"""
|
||||
get dependencies for the specific package base if available
|
||||
|
||||
Args:
|
||||
package_base(str | None): package base to search
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Returns:
|
||||
Dependencies: changes for the package base if available
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> list[Dependencies]:
|
||||
return [
|
||||
Dependencies(
|
||||
row["package_base"],
|
||||
{
|
||||
Path(path): packages
|
||||
for path, packages in row["dependencies"].items()
|
||||
}
|
||||
)
|
||||
for row in connection.execute(
|
||||
"""
|
||||
select package_base, dependencies from package_dependencies
|
||||
where (:package_base is null or package_base = :package_base)
|
||||
and repository = :repository
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"repository": repository_id.id,
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
return self.with_connection(run)
|
||||
|
||||
def dependencies_insert(self, dependencies: Dependencies, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
insert package dependencies
|
||||
|
||||
Args:
|
||||
dependencies(Dependencies): package dependencies
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into package_dependencies
|
||||
(package_base, repository, dependencies)
|
||||
values
|
||||
(:package_base, :repository, :dependencies)
|
||||
on conflict (package_base, repository) do update set
|
||||
dependencies = :dependencies
|
||||
""",
|
||||
{
|
||||
"package_base": dependencies.package_base,
|
||||
"repository": repository_id.id,
|
||||
"dependencies": {
|
||||
str(path): packages
|
||||
for path, packages in dependencies.paths.items()
|
||||
}
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def dependencies_remove(self, package_base: str | None, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
remove packages dependencies
|
||||
|
||||
Args:
|
||||
package_base(str | None): optional filter by package base
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
delete from package_dependencies
|
||||
where (:package_base is null or package_base = :package_base)
|
||||
and repository = :repository
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
@ -150,34 +150,6 @@ class PackageOperations(Operations):
|
||||
""",
|
||||
package_list)
|
||||
|
||||
@staticmethod
|
||||
def _package_update_insert_status(connection: Connection, package_base: str, status: BuildStatus,
|
||||
repository_id: RepositoryId) -> None:
|
||||
"""
|
||||
insert base package status into table
|
||||
|
||||
Args:
|
||||
connection(Connection): database connection
|
||||
package_base(str): package base name
|
||||
status(BuildStatus): new build status
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
"""
|
||||
connection.execute(
|
||||
"""
|
||||
insert into package_statuses
|
||||
(package_base, status, last_updated, repository)
|
||||
values
|
||||
(:package_base, :status, :last_updated, :repository)
|
||||
on conflict (package_base, repository) do update set
|
||||
status = :status, last_updated = :last_updated
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"status": status.status.value,
|
||||
"last_updated": status.timestamp,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
@staticmethod
|
||||
def _packages_get_select_package_bases(connection: Connection, repository_id: RepositoryId) -> dict[str, Package]:
|
||||
"""
|
||||
@ -277,20 +249,18 @@ class PackageOperations(Operations):
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def package_update(self, package: Package, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
|
||||
def package_update(self, package: Package, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
update package status
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
status(BuildStatus): new build status
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
self._package_update_insert_base(connection, package, repository_id)
|
||||
self._package_update_insert_status(connection, package.base, status, repository_id)
|
||||
self._package_update_insert_packages(connection, package, repository_id)
|
||||
self._package_remove_packages(connection, package.base, package.packages.keys(), repository_id)
|
||||
|
||||
@ -336,3 +306,33 @@ class PackageOperations(Operations):
|
||||
package_base: package.remote
|
||||
for package_base, package in self.with_connection(run).items()
|
||||
}
|
||||
|
||||
def status_update(self, package_base: str, status: BuildStatus, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
insert base package status into table
|
||||
|
||||
Args:
|
||||
package_base(str): package base name
|
||||
status(BuildStatus): new build status
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into package_statuses
|
||||
(package_base, status, last_updated, repository)
|
||||
values
|
||||
(:package_base, :status, :last_updated, :repository)
|
||||
on conflict (package_base, repository) do update set
|
||||
status = :status, last_updated = :last_updated
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"status": status.status.value,
|
||||
"last_updated": status.timestamp,
|
||||
"repository": repository_id.id,
|
||||
})
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
@ -25,12 +25,19 @@ from typing import Self
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.migrations import Migrations
|
||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, LogsOperations, \
|
||||
PackageOperations, PatchOperations
|
||||
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
|
||||
DependenciesOperations, LogsOperations, PackageOperations, PatchOperations
|
||||
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations, PackageOperations, PatchOperations):
|
||||
class SQLite(
|
||||
AuthOperations,
|
||||
BuildOperations,
|
||||
ChangesOperations,
|
||||
DependenciesOperations,
|
||||
LogsOperations,
|
||||
PackageOperations,
|
||||
PatchOperations):
|
||||
"""
|
||||
wrapper for sqlite3 database
|
||||
|
||||
@ -94,3 +101,21 @@ class SQLite(AuthOperations, BuildOperations, ChangesOperations, LogsOperations,
|
||||
if configuration.getboolean("settings", "apply_migrations", fallback=True):
|
||||
self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
|
||||
paths.chown(self.path)
|
||||
|
||||
def package_clear(self, package_base: str) -> None:
|
||||
"""
|
||||
completely remove package from all tables
|
||||
|
||||
Args:
|
||||
package_base(str): package base to remove
|
||||
|
||||
Examples:
|
||||
This method completely removes the package from all tables and must be used, e.g. on package removal::
|
||||
|
||||
>>> database.package_clear("ahriman")
|
||||
"""
|
||||
self.build_queue_clear(package_base)
|
||||
self.patches_remove(package_base, [])
|
||||
self.logs_remove(package_base, None)
|
||||
self.changes_remove(package_base)
|
||||
self.dependencies_remove(package_base)
|
||||
|
@ -219,6 +219,21 @@ class PackageInfoError(RuntimeError):
|
||||
RuntimeError.__init__(self, f"There are errors during reading package information: `{details}`")
|
||||
|
||||
|
||||
class PacmanError(RuntimeError):
|
||||
"""
|
||||
exception in case of pacman operation errors
|
||||
"""
|
||||
|
||||
def __init__(self, details: Any) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
details(Any): error details
|
||||
"""
|
||||
RuntimeError.__init__(self, f"Could not perform operation with pacman: `{details}`")
|
||||
|
||||
|
||||
class PathError(ValueError):
|
||||
"""
|
||||
exception which will be raised on path which is not belong to root directory
|
||||
@ -330,5 +345,5 @@ class UnsafeRunError(RuntimeError):
|
||||
root_uid(int): ID of the owner of root directory
|
||||
"""
|
||||
RuntimeError.__init__(self, f"Current UID {current_uid} differs from root owner {root_uid}. "
|
||||
f"Note that for the most actions it is unsafe to run application as different user."
|
||||
f" If you are 100% sure that it must be there try --unsafe option")
|
||||
f"Note that for the most actions it is unsafe to run application as different user."
|
||||
f" If you are 100% sure that it must be there try --unsafe option")
|
||||
|
@ -38,7 +38,7 @@ class SyncHttpClient(LazyLogging):
|
||||
Attributes:
|
||||
auth(tuple[str, str] | None): HTTP basic auth object if set
|
||||
suppress_errors(bool): suppress logging of request errors
|
||||
timeout(int): HTTP request timeout in seconds
|
||||
timeout(int | None): HTTP request timeout in seconds
|
||||
"""
|
||||
|
||||
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
|
||||
@ -60,7 +60,7 @@ class SyncHttpClient(LazyLogging):
|
||||
password = configuration.get(section, "password", fallback=None)
|
||||
self.auth = (username, password) if username and password else None
|
||||
|
||||
self.timeout = configuration.getint(section, "timeout", fallback=30)
|
||||
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
|
||||
self.suppress_errors = suppress_errors
|
||||
|
||||
@cached_property
|
||||
@ -90,25 +90,27 @@ class SyncHttpClient(LazyLogging):
|
||||
result: str = exception.response.text if exception.response is not None else ""
|
||||
return result
|
||||
|
||||
def make_request(self, method: Literal["DELETE", "GET", "POST", "PUT"], url: str, *,
|
||||
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
|
||||
headers: dict[str, str] | None = None,
|
||||
params: list[tuple[str, str]] | None = None,
|
||||
data: Any | None = None,
|
||||
json: dict[str, Any] | None = None,
|
||||
files: dict[str, MultipartType] | None = None,
|
||||
stream: bool | None = None,
|
||||
session: requests.Session | None = None,
|
||||
suppress_errors: bool | None = None) -> requests.Response:
|
||||
"""
|
||||
perform request with specified parameters
|
||||
|
||||
Args:
|
||||
method(Literal["DELETE", "GET", "POST", "PUT"]): HTTP method to call
|
||||
method(Literal["DELETE", "GET", "HEAD", "POST", "PUT"]): HTTP method to call
|
||||
url(str): remote url to call
|
||||
headers(dict[str, str] | None, optional): request headers (Default value = None)
|
||||
params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None)
|
||||
data(Any | None, optional): request raw data parameters (Default value = None)
|
||||
json(dict[str, Any] | None, optional): request json parameters (Default value = None)
|
||||
files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None)
|
||||
stream(bool | None, optional): handle response as stream (Default value = None)
|
||||
session(requests.Session | None, optional): session object if any (Default value = None)
|
||||
suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none
|
||||
set, the instance-wide value will be used (Default value = None)
|
||||
@ -124,7 +126,7 @@ class SyncHttpClient(LazyLogging):
|
||||
|
||||
try:
|
||||
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json,
|
||||
auth=self.auth, timeout=self.timeout)
|
||||
stream=stream, auth=self.auth, timeout=self.timeout)
|
||||
response.raise_for_status()
|
||||
return response
|
||||
except requests.HTTPError as ex:
|
||||
|
@ -92,7 +92,7 @@ class HttpLogHandler(logging.Handler):
|
||||
return # in case if no package base supplied we need just skip log message
|
||||
|
||||
try:
|
||||
self.reporter.package_logs(log_record_id, record)
|
||||
self.reporter.package_logs_add(log_record_id, record.created, record.getMessage())
|
||||
except Exception:
|
||||
if self.suppress_errors:
|
||||
return
|
||||
|
@ -106,7 +106,7 @@ class RemoteCall(Report):
|
||||
"aur": self.update_aur,
|
||||
"local": self.update_local,
|
||||
"manual": self.update_manual,
|
||||
})
|
||||
})
|
||||
response_json = response.json()
|
||||
|
||||
process_id: str = response_json["process_id"]
|
||||
|
@ -29,6 +29,7 @@ from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.util import safe_filename
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_archive import PackageArchive
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.result import Result
|
||||
@ -77,6 +78,10 @@ class Executor(PackageInfo, Cleaner):
|
||||
last_commit_sha = build_single(single, Path(dir_name), packager.packager_id)
|
||||
# clear changes and update commit hash
|
||||
self.reporter.package_changes_set(single.base, Changes(last_commit_sha))
|
||||
# update dependencies list
|
||||
dependencies = PackageArchive(self.paths.build_directory, single).depends_on()
|
||||
self.database.dependencies_insert(dependencies)
|
||||
# update result set
|
||||
result.add_updated(single)
|
||||
except Exception:
|
||||
self.reporter.set_failed(single.base)
|
||||
@ -98,10 +103,7 @@ class Executor(PackageInfo, Cleaner):
|
||||
def remove_base(package_base: str) -> None:
|
||||
try:
|
||||
self.paths.tree_clear(package_base) # remove all internal files
|
||||
self.database.build_queue_clear(package_base)
|
||||
self.database.patches_remove(package_base, [])
|
||||
self.database.logs_remove(package_base, None)
|
||||
self.database.changes_remove(package_base)
|
||||
self.database.package_clear(package_base)
|
||||
self.reporter.package_remove(package_base) # we only update status page in case of base removal
|
||||
except Exception:
|
||||
self.logger.exception("could not remove base %s", package_base)
|
||||
@ -117,7 +119,8 @@ class Executor(PackageInfo, Cleaner):
|
||||
|
||||
# build package list based on user input
|
||||
result = Result()
|
||||
requested = set(packages)
|
||||
packages = set(packages) # remove duplicates
|
||||
requested = packages | {f"{package}-debug" for package in packages} # append debug packages
|
||||
for local in self.packages():
|
||||
if local.base in packages or all(package in requested for package in local.packages):
|
||||
packages_to_remove.update({
|
||||
@ -136,7 +139,7 @@ class Executor(PackageInfo, Cleaner):
|
||||
|
||||
# check for packages which were requested to remove, but weren't found locally
|
||||
# it might happen for example, if there were no success build before
|
||||
for unknown in requested:
|
||||
for unknown in packages:
|
||||
if unknown in packages_to_remove or unknown in bases_to_remove:
|
||||
continue
|
||||
bases_to_remove.append(unknown)
|
||||
|
@ -86,14 +86,21 @@ class PackageInfo(RepositoryProperties):
|
||||
|
||||
return Changes(last_commit_sha, changes)
|
||||
|
||||
def packages(self) -> list[Package]:
|
||||
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
|
||||
Args:
|
||||
filter_packages(Iterable[str] | None, optional): filter packages list by specified only
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
"""
|
||||
return self.load_archives(filter(package_like, self.paths.repository.iterdir()))
|
||||
packages = self.load_archives(filter(package_like, self.paths.repository.iterdir()))
|
||||
if filter_packages:
|
||||
packages = [package for package in packages if package.base in filter_packages]
|
||||
|
||||
return packages
|
||||
|
||||
def packages_built(self) -> list[Path]:
|
||||
"""
|
||||
|
@ -75,7 +75,7 @@ class RepositoryProperties(LazyLogging):
|
||||
self.pacman = Pacman(repository_id, configuration, refresh_database=refresh_pacman_database)
|
||||
self.sign = GPG(configuration)
|
||||
self.repo = Repo(self.name, self.paths, self.sign.repository_sign_args)
|
||||
self.reporter = Client.load(repository_id, configuration, report=report)
|
||||
self.reporter = Client.load(repository_id, configuration, database, report=report)
|
||||
self.triggers = TriggerLoader.load(repository_id, configuration)
|
||||
|
||||
@property
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
@ -55,17 +56,13 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
continue
|
||||
raise UnknownPackageError(package.base)
|
||||
|
||||
local_versions = {package.base: package.version for package in self.packages()}
|
||||
|
||||
result: list[Package] = []
|
||||
for local in self.packages():
|
||||
with self.in_package_context(local.base, local_versions.get(local.base)):
|
||||
for local in self.packages(filter_packages):
|
||||
with self.in_package_context(local.base, local.version):
|
||||
if not local.remote.is_remote:
|
||||
continue # avoid checking local packages
|
||||
if local.base in self.ignore_list:
|
||||
continue
|
||||
if filter_packages and local.base not in filter_packages:
|
||||
continue
|
||||
|
||||
try:
|
||||
remote = load_remote(local)
|
||||
@ -82,6 +79,47 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
|
||||
return result
|
||||
|
||||
def updates_dependencies(self, filter_packages: Iterable[str]) -> list[Package]:
|
||||
"""
|
||||
check packages which ae required to be rebuilt based on dynamic dependencies (e.g. linking, modules paths, etc.)
|
||||
|
||||
Args:
|
||||
filter_packages(Iterable[str]): do not check every package just specified in the list
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages for which there is breaking linking
|
||||
"""
|
||||
def extract_files(lookup_packages: Iterable[str]) -> dict[Path, set[str]]:
|
||||
database_files = self.pacman.files(lookup_packages)
|
||||
files: dict[Path, set[str]] = {}
|
||||
for package_name, package_files in database_files.items(): # invert map
|
||||
for package_file in package_files:
|
||||
files.setdefault(package_file, set()).add(package_name)
|
||||
|
||||
return files
|
||||
|
||||
dependencies = {dependency.package_base: dependency for dependency in self.database.dependencies_get()}
|
||||
|
||||
result: list[Package] = []
|
||||
for package in self.packages(filter_packages):
|
||||
if package.base not in dependencies:
|
||||
continue # skip check if no package dependencies found
|
||||
|
||||
required = dependencies[package.base].paths
|
||||
required_packages = {dep for dep_packages in required.values() for dep in dep_packages}
|
||||
filesystem = extract_files(required_packages)
|
||||
|
||||
for path, packages in required.items():
|
||||
found = filesystem.get(path, set())
|
||||
if found.intersection(packages):
|
||||
continue
|
||||
|
||||
# there are no packages found in filesystem with the same paths
|
||||
result.append(package)
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
def updates_local(self, *, vcs: bool) -> list[Package]:
|
||||
"""
|
||||
check local packages for updates
|
||||
|
@ -17,16 +17,18 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# pylint: disable=too-many-public-methods
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -36,22 +38,31 @@ class Client:
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def load(repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Client:
|
||||
def load(repository_id: RepositoryId, configuration: Configuration, database: SQLite | None = None, *,
|
||||
report: bool = True) -> Client:
|
||||
"""
|
||||
load client from settings
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
database(SQLite | None, optional): database instance (Default value = None)
|
||||
report(bool, optional): force enable or disable reporting (Default value = True)
|
||||
|
||||
Returns:
|
||||
Client: client according to current settings
|
||||
"""
|
||||
def make_local_client() -> Client:
|
||||
if database is None:
|
||||
return Client()
|
||||
|
||||
from ahriman.core.status.local_client import LocalClient
|
||||
return LocalClient(repository_id, database)
|
||||
|
||||
if not report:
|
||||
return Client()
|
||||
return make_local_client()
|
||||
if not configuration.getboolean("status", "enabled", fallback=True): # global switch
|
||||
return Client()
|
||||
return make_local_client()
|
||||
|
||||
# new-style section
|
||||
address = configuration.get("status", "address", fallback=None)
|
||||
@ -65,7 +76,8 @@ class Client:
|
||||
if address or legacy_address or (host and port) or socket:
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
return WebClient(repository_id, configuration)
|
||||
return Client()
|
||||
|
||||
return make_local_client()
|
||||
|
||||
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
@ -74,7 +86,11 @@ class Client:
|
||||
Args:
|
||||
package(Package): package properties
|
||||
status(BuildStatusEnum): current package build status
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
@ -85,9 +101,11 @@ class Client:
|
||||
|
||||
Returns:
|
||||
Changes: package changes if available and empty object otherwise
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
del package_base
|
||||
return Changes()
|
||||
raise NotImplementedError
|
||||
|
||||
def package_changes_set(self, package_base: str, changes: Changes) -> None:
|
||||
"""
|
||||
@ -96,7 +114,38 @@ class Client:
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
changes(Changes): changes descriptor
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_dependencies_get(self, package_base: str | None) -> list[Dependencies]:
|
||||
"""
|
||||
get package dependencies
|
||||
|
||||
Args:
|
||||
package_base(str | None): package base to retrieve
|
||||
|
||||
Returns:
|
||||
list[Dependencies]: package implicit dependencies if available
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_dependencies_set(self, dependencies: Dependencies) -> None:
|
||||
"""
|
||||
update package dependencies
|
||||
|
||||
Args:
|
||||
dependencies(Dependencies): dependencies descriptor
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
@ -107,35 +156,118 @@ class Client:
|
||||
|
||||
Returns:
|
||||
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
|
||||
"""
|
||||
del package_base
|
||||
return []
|
||||
|
||||
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
record(logging.LogRecord): log record to post to api
|
||||
created(float): log created timestamp
|
||||
message(str): log message
|
||||
"""
|
||||
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
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[tuple[float, str]]: package logs
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_logs_remove(self, package_base: str, version: str | None) -> None:
|
||||
"""
|
||||
remove package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_patches_add(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
create or update package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
patch(PkgbuildPatch): package patch
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
get package patches
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
variable(str | None): optional filter by patch variable
|
||||
|
||||
Returns:
|
||||
list[PkgbuildPatch]: list of patches for the specified package
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_patches_remove(self, package_base: str, variable: str | None) -> None:
|
||||
"""
|
||||
remove package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If None set, all patches will be removed
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_remove(self, package_base: str) -> None:
|
||||
"""
|
||||
remove packages from watcher
|
||||
|
||||
Args:
|
||||
package_base(str): package base to remove
|
||||
"""
|
||||
|
||||
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_set(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike :func:`package_add()` it does not update package properties
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
status(BuildStatusEnum): current package build status
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_building(self, package_base: str) -> None:
|
||||
"""
|
||||
@ -144,7 +276,7 @@ class Client:
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
"""
|
||||
return self.package_update(package_base, BuildStatusEnum.Building)
|
||||
return self.package_set(package_base, BuildStatusEnum.Building)
|
||||
|
||||
def set_failed(self, package_base: str) -> None:
|
||||
"""
|
||||
@ -153,7 +285,7 @@ class Client:
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
"""
|
||||
return self.package_update(package_base, BuildStatusEnum.Failed)
|
||||
return self.package_set(package_base, BuildStatusEnum.Failed)
|
||||
|
||||
def set_pending(self, package_base: str) -> None:
|
||||
"""
|
||||
@ -162,7 +294,7 @@ class Client:
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
"""
|
||||
return self.package_update(package_base, BuildStatusEnum.Pending)
|
||||
return self.package_set(package_base, BuildStatusEnum.Pending)
|
||||
|
||||
def set_success(self, package: Package) -> None:
|
||||
"""
|
||||
|
208
src/ahriman/core/status/local_client.py
Normal file
208
src/ahriman/core/status/local_client.py
Normal file
@ -0,0 +1,208 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 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.database import SQLite
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class LocalClient(Client):
|
||||
"""
|
||||
local database handler
|
||||
|
||||
Attributes:
|
||||
database(SQLite): database instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
database(SQLite): database instance:
|
||||
"""
|
||||
self.database = database
|
||||
self.repository_id = repository_id
|
||||
|
||||
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
add new package with status
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
status(BuildStatusEnum): current package build status
|
||||
"""
|
||||
self.database.package_update(package, self.repository_id)
|
||||
self.database.status_update(package.base, BuildStatus(status), self.repository_id)
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
|
||||
Returns:
|
||||
Changes: package changes if available and empty object otherwise
|
||||
"""
|
||||
return self.database.changes_get(package_base, self.repository_id)
|
||||
|
||||
def package_changes_set(self, package_base: str, changes: Changes) -> None:
|
||||
"""
|
||||
update package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
changes(Changes): changes descriptor
|
||||
"""
|
||||
self.database.changes_insert(package_base, changes, self.repository_id)
|
||||
|
||||
def package_dependencies_get(self, package_base: str | None) -> list[Dependencies]:
|
||||
"""
|
||||
get package dependencies
|
||||
|
||||
Args:
|
||||
package_base(str | None): package base to retrieve
|
||||
|
||||
Returns:
|
||||
list[Dependencies]: package implicit dependencies if available
|
||||
"""
|
||||
return self.database.dependencies_get(package_base, self.repository_id)
|
||||
|
||||
def package_dependencies_set(self, dependencies: Dependencies) -> None:
|
||||
"""
|
||||
update package dependencies
|
||||
|
||||
Args:
|
||||
dependencies(Dependencies): dependencies descriptor
|
||||
"""
|
||||
self.database.dependencies_insert(dependencies, self.repository_id)
|
||||
|
||||
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get package status
|
||||
|
||||
Args:
|
||||
package_base(str | None): package base to get
|
||||
|
||||
Returns:
|
||||
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
|
||||
"""
|
||||
packages = self.database.packages_get()
|
||||
if package_base is None:
|
||||
return packages
|
||||
return [(package, status) for package, status in packages if package.base == package_base]
|
||||
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
created(float): log created timestamp
|
||||
message(str): log message
|
||||
"""
|
||||
self.database.logs_insert(log_record_id, created, message, self.repository_id)
|
||||
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
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[tuple[float, str]]: package logs
|
||||
"""
|
||||
return self.database.logs_get(package_base, limit, offset, self.repository_id)
|
||||
|
||||
def package_logs_remove(self, package_base: str, version: str | None) -> None:
|
||||
"""
|
||||
remove package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
||||
"""
|
||||
self.database.logs_remove(package_base, version, self.repository_id)
|
||||
|
||||
def package_patches_add(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
create or update package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
patch(PkgbuildPatch): package patch
|
||||
"""
|
||||
self.database.patches_insert(package_base, [patch])
|
||||
|
||||
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
get package patches
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
variable(str | None): optional filter by patch variable
|
||||
|
||||
Returns:
|
||||
list[PkgbuildPatch]: list of patches for the specified package
|
||||
"""
|
||||
variables = [variable] if variable is not None else None
|
||||
return self.database.patches_list(package_base, variables).get(package_base, [])
|
||||
|
||||
def package_patches_remove(self, package_base: str, variable: str | None) -> None:
|
||||
"""
|
||||
remove package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If None set, all patches will be removed
|
||||
"""
|
||||
variables = [variable] if variable is not None else None
|
||||
self.database.patches_remove(package_base, variables)
|
||||
|
||||
def package_remove(self, package_base: str) -> None:
|
||||
"""
|
||||
remove packages from watcher
|
||||
|
||||
Args:
|
||||
package_base(str): package base to remove
|
||||
"""
|
||||
self.database.package_remove(package_base, self.repository_id)
|
||||
self.package_logs_remove(package_base, None)
|
||||
|
||||
def package_set(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike :func:`package_add()` it does not update package properties
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
status(BuildStatusEnum): current package build status
|
||||
"""
|
||||
self.database.status_update(package_base, BuildStatus(status), self.repository_id)
|
@ -19,15 +19,15 @@
|
||||
#
|
||||
from threading import Lock
|
||||
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Watcher(LazyLogging):
|
||||
@ -35,21 +35,18 @@ class Watcher(LazyLogging):
|
||||
package status watcher
|
||||
|
||||
Attributes:
|
||||
database(SQLite): database instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
client(Client): reporter instance
|
||||
status(BuildStatus): daemon status
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, database: SQLite) -> None:
|
||||
def __init__(self, client: Client) -> None:
|
||||
"""
|
||||
default constructor
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
database(SQLite): database instance
|
||||
client(Client): reporter instance
|
||||
"""
|
||||
self.repository_id = repository_id
|
||||
self.database = database
|
||||
self.client = client
|
||||
|
||||
self._lock = Lock()
|
||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||
@ -76,7 +73,7 @@ class Watcher(LazyLogging):
|
||||
with self._lock:
|
||||
self._known = {
|
||||
package.base: (package, status)
|
||||
for package, status in self.database.packages_get(self.repository_id)
|
||||
for package, status in self.client.package_get(None)
|
||||
}
|
||||
|
||||
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||
@ -91,8 +88,8 @@ class Watcher(LazyLogging):
|
||||
Returns:
|
||||
list[tuple[float, str]]: package logs
|
||||
"""
|
||||
self.package_get(package_base)
|
||||
return self.database.logs_get(package_base, limit, offset, self.repository_id)
|
||||
_ = self.package_get(package_base)
|
||||
return self.client.package_logs_get(package_base, limit, offset)
|
||||
|
||||
def logs_remove(self, package_base: str, version: str | None) -> None:
|
||||
"""
|
||||
@ -100,24 +97,24 @@ class Watcher(LazyLogging):
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str): package versio
|
||||
version(str): package version
|
||||
"""
|
||||
self.database.logs_remove(package_base, version, self.repository_id)
|
||||
self.client.package_logs_remove(package_base, version)
|
||||
|
||||
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None:
|
||||
def logs_update(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
"""
|
||||
make new log record into database
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
created(float): log created timestamp
|
||||
record(str): log record
|
||||
message(str): log message
|
||||
"""
|
||||
if self._last_log_record_id != log_record_id:
|
||||
# there is new log record, so we remove old ones
|
||||
self.logs_remove(log_record_id.package_base, log_record_id.version)
|
||||
self._last_log_record_id = log_record_id
|
||||
self.database.logs_insert(log_record_id, created, record, self.repository_id)
|
||||
self.client.package_logs_add(log_record_id, created, message)
|
||||
|
||||
def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
@ -129,8 +126,24 @@ class Watcher(LazyLogging):
|
||||
Returns:
|
||||
Changes: package changes if available
|
||||
"""
|
||||
self.package_get(package_base)
|
||||
return self.database.changes_get(package_base, self.repository_id)
|
||||
_ = self.package_get(package_base)
|
||||
return self.client.package_changes_get(package_base)
|
||||
|
||||
def package_dependencies_get(self, package_base: str) -> Dependencies:
|
||||
"""
|
||||
retrieve package dependencies
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
Dependencies: package dependencies if available
|
||||
"""
|
||||
_ = self.package_get(package_base)
|
||||
try:
|
||||
return next(iter(self.client.package_dependencies_get(package_base)))
|
||||
except StopIteration:
|
||||
return Dependencies(package_base)
|
||||
|
||||
def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
|
||||
"""
|
||||
@ -160,7 +173,7 @@ class Watcher(LazyLogging):
|
||||
"""
|
||||
with self._lock:
|
||||
self._known.pop(package_base, None)
|
||||
self.database.package_remove(package_base, self.repository_id)
|
||||
self.client.package_remove(package_base)
|
||||
self.logs_remove(package_base, None)
|
||||
|
||||
def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
|
||||
@ -174,10 +187,9 @@ class Watcher(LazyLogging):
|
||||
"""
|
||||
if package is None:
|
||||
package, _ = self.package_get(package_base)
|
||||
full_status = BuildStatus(status)
|
||||
with self._lock:
|
||||
self._known[package_base] = (package, full_status)
|
||||
self.database.package_update(package, full_status, self.repository_id)
|
||||
self._known[package_base] = (package, BuildStatus(status))
|
||||
self.client.package_set(package_base, status)
|
||||
|
||||
def patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
@ -192,8 +204,7 @@ class Watcher(LazyLogging):
|
||||
"""
|
||||
# patches are package base based, we don't know (and don't differentiate) to which package does them belong
|
||||
# so here we skip checking if package exists or not
|
||||
variables = [variable] if variable is not None else None
|
||||
return self.database.patches_list(package_base, variables).get(package_base, [])
|
||||
return self.client.package_patches_get(package_base, variable)
|
||||
|
||||
def patches_remove(self, package_base: str, variable: str) -> None:
|
||||
"""
|
||||
@ -203,7 +214,7 @@ class Watcher(LazyLogging):
|
||||
package_base(str): package base
|
||||
variable(str): patch variable name
|
||||
"""
|
||||
self.database.patches_remove(package_base, [variable])
|
||||
self.client.package_patches_remove(package_base, variable)
|
||||
|
||||
def patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
@ -213,7 +224,7 @@ class Watcher(LazyLogging):
|
||||
package_base(str): package base
|
||||
patch(PkgbuildPatch): package patch
|
||||
"""
|
||||
self.database.patches_insert(package_base, [patch])
|
||||
self.client.package_patches_add(package_base, patch)
|
||||
|
||||
def status_update(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
|
@ -18,7 +18,6 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
from urllib.parse import quote_plus as urlencode
|
||||
|
||||
@ -27,9 +26,11 @@ from ahriman.core.http import SyncAhrimanClient
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -92,10 +93,22 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
str: full url for web service for logs
|
||||
str: full url for web service for changes
|
||||
"""
|
||||
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/changes"
|
||||
|
||||
def _dependencies_url(self, package_base: str = "") -> str:
|
||||
"""
|
||||
get url for the dependencies api
|
||||
|
||||
Args:
|
||||
package_base(str, optional): package base (Default value = "")
|
||||
|
||||
Returns:
|
||||
str: full url for web service for dependencies
|
||||
"""
|
||||
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/dependencies"
|
||||
|
||||
def _logs_url(self, package_base: str) -> str:
|
||||
"""
|
||||
get url for the logs api
|
||||
@ -110,7 +123,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
|
||||
def _package_url(self, package_base: str = "") -> str:
|
||||
"""
|
||||
url generator
|
||||
package url generator
|
||||
|
||||
Args:
|
||||
package_base(str, optional): package base to generate url (Default value = "")
|
||||
@ -121,6 +134,20 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
suffix = f"/{urlencode(package_base)}" if package_base else ""
|
||||
return f"{self.address}/api/v1/packages{suffix}"
|
||||
|
||||
def _patches_url(self, package_base: str, variable: str = "") -> str:
|
||||
"""
|
||||
patches url generator
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
variable(str, optional): patch variable name to generate url (Default value = "")
|
||||
|
||||
Returns:
|
||||
str: full url of web service for the package patch
|
||||
"""
|
||||
suffix = f"/{urlencode(variable)}" if variable else ""
|
||||
return f"{self.address}/api/v1/packages/{urlencode(package_base)}/patches{suffix}"
|
||||
|
||||
def _status_url(self) -> str:
|
||||
"""
|
||||
get url for the status api
|
||||
@ -177,6 +204,37 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
self.make_request("POST", self._changes_url(package_base),
|
||||
params=self.repository_id.query(), json=changes.view())
|
||||
|
||||
def package_dependencies_get(self, package_base: str | None) -> list[Dependencies]:
|
||||
"""
|
||||
get package dependencies
|
||||
|
||||
Args:
|
||||
package_base(str | None): package base to retrieve
|
||||
|
||||
Returns:
|
||||
list[Dependencies]: package implicit dependencies if available
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
response = self.make_request("GET", self._dependencies_url(package_base or ""),
|
||||
params=self.repository_id.query())
|
||||
response_json = response.json()
|
||||
|
||||
dependencies = response_json if package_base is None else [response_json]
|
||||
return [Dependencies.from_json(dependencies) for dependencies in dependencies]
|
||||
|
||||
return []
|
||||
|
||||
def package_dependencies_set(self, dependencies: Dependencies) -> None:
|
||||
"""
|
||||
update package dependencies
|
||||
|
||||
Args:
|
||||
dependencies(Dependencies): dependencies descriptor
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._dependencies_url(dependencies.package_base),
|
||||
params=self.repository_id.query(), json=dependencies.view())
|
||||
|
||||
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get package status
|
||||
@ -199,17 +257,18 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
|
||||
return []
|
||||
|
||||
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
record(logging.LogRecord): log record to post to api
|
||||
created(float): log created timestamp
|
||||
message(str): log message
|
||||
"""
|
||||
payload = {
|
||||
"created": record.created,
|
||||
"message": record.getMessage(),
|
||||
"created": created,
|
||||
"message": message,
|
||||
"version": log_record_id.version,
|
||||
}
|
||||
|
||||
@ -219,6 +278,83 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
self.make_request("POST", self._logs_url(log_record_id.package_base),
|
||||
params=self.repository_id.query(), json=payload, suppress_errors=True)
|
||||
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
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[tuple[float, str]]: package logs
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
||||
response_json = response.json()
|
||||
|
||||
return [(record["created"], record["message"]) for record in response_json]
|
||||
|
||||
return []
|
||||
|
||||
def package_logs_remove(self, package_base: str, version: str | None) -> None:
|
||||
"""
|
||||
remove package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If None set, all logs will be removed
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
query = self.repository_id.query()
|
||||
if version is not None:
|
||||
query += [("version", version)]
|
||||
self.make_request("DELETE", self._logs_url(package_base), params=query)
|
||||
|
||||
def package_patches_add(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
create or update package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
patch(PkgbuildPatch): package patch
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("POST", self._patches_url(package_base), json=patch.view())
|
||||
|
||||
def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
get package patches
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
variable(str | None): optional filter by patch variable
|
||||
|
||||
Returns:
|
||||
list[PkgbuildPatch]: list of patches for the specified package
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
response = self.make_request("GET", self._patches_url(package_base, variable or ""))
|
||||
response_json = response.json()
|
||||
|
||||
patches = response_json if variable is None else [response_json]
|
||||
return [PkgbuildPatch.from_json(patch) for patch in patches]
|
||||
|
||||
return []
|
||||
|
||||
def package_patches_remove(self, package_base: str, variable: str | None) -> None:
|
||||
"""
|
||||
remove package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If None set, all patches will be removed
|
||||
"""
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("DELETE", self._patches_url(package_base, variable or ""))
|
||||
|
||||
def package_remove(self, package_base: str) -> None:
|
||||
"""
|
||||
remove packages from watcher
|
||||
@ -229,7 +365,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
with contextlib.suppress(Exception):
|
||||
self.make_request("DELETE", self._package_url(package_base), params=self.repository_id.query())
|
||||
|
||||
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
def package_set(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package build status. Unlike :func:`package_add()` it does not update package properties
|
||||
|
||||
|
@ -68,4 +68,6 @@ class PackageCreator:
|
||||
database: SQLite = ctx.get(ContextKey("database", SQLite))
|
||||
_, repository_id = self.configuration.check_loaded()
|
||||
package = Package.from_build(local_path, repository_id.architecture, None)
|
||||
database.package_update(package, BuildStatus())
|
||||
|
||||
database.package_update(package)
|
||||
database.status_update(package.base, BuildStatus())
|
||||
|
@ -183,11 +183,12 @@ post_install() {{
|
||||
Returns:
|
||||
str: package() function for PKGBUILD
|
||||
"""
|
||||
# somehow autopep thinks that construction inside contains valid python code and reformats it
|
||||
return f"""{{
|
||||
install -Dm644 "{Path("$srcdir") / f"{self.name}.gpg"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}.gpg"}"
|
||||
install -Dm644 "{Path("$srcdir") / f"{self.name}-revoked"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-revoked"}"
|
||||
install -Dm644 "{Path("$srcdir") / f"{self.name}-trusted"}" "{Path("$pkgdir") / "usr" / "share" / "pacman" / "keyrings" / f"{self.name}-trusted"}"
|
||||
}}"""
|
||||
}}""" # nopep8
|
||||
|
||||
def sources(self) -> dict[str, Callable[[Path], None]]:
|
||||
"""
|
||||
|
@ -162,7 +162,8 @@ class GitHub(Upload, HttpUpload):
|
||||
Returns:
|
||||
dict[str, Any] | None: GitHub API release object if release found and None otherwise
|
||||
"""
|
||||
url = f"https://api.github.com/repos/{self.github_owner}/{self.github_repository}/releases/tags/{self.github_release_tag}"
|
||||
url = f"https://api.github.com/repos/{self.github_owner}/{
|
||||
self.github_repository}/releases/tags/{self.github_release_tag}"
|
||||
try:
|
||||
response = self.make_request("GET", url)
|
||||
release: dict[str, Any] = response.json()
|
||||
|
@ -349,7 +349,7 @@ def pretty_datetime(timestamp: datetime.datetime | float | int | None) -> str:
|
||||
if timestamp is None:
|
||||
return ""
|
||||
if isinstance(timestamp, (int, float)):
|
||||
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
|
||||
timestamp = datetime.datetime.fromtimestamp(timestamp, datetime.UTC)
|
||||
return timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
@ -505,7 +505,7 @@ def utcnow() -> datetime.datetime:
|
||||
Returns:
|
||||
datetime.datetime: current time in UTC
|
||||
"""
|
||||
return datetime.datetime.utcnow()
|
||||
return datetime.datetime.now(datetime.UTC)
|
||||
|
||||
|
||||
def walk(directory_path: Path) -> Generator[Path, None, None]:
|
||||
|
@ -137,8 +137,8 @@ class AURPackage:
|
||||
description=package.desc,
|
||||
num_votes=0,
|
||||
popularity=0.0,
|
||||
first_submitted=datetime.datetime.utcfromtimestamp(0),
|
||||
last_modified=datetime.datetime.utcfromtimestamp(package.builddate),
|
||||
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
|
||||
last_modified=datetime.datetime.fromtimestamp(package.builddate, datetime.UTC),
|
||||
url_path="",
|
||||
url=package.url,
|
||||
out_of_date=None,
|
||||
@ -175,13 +175,11 @@ class AURPackage:
|
||||
description=dump["pkgdesc"],
|
||||
num_votes=0,
|
||||
popularity=0.0,
|
||||
first_submitted=datetime.datetime.utcfromtimestamp(0),
|
||||
last_modified=datetime.datetime.strptime(dump["last_update"], "%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
|
||||
last_modified=datetime.datetime.fromisoformat(dump["last_update"]),
|
||||
url_path="",
|
||||
url=dump["url"],
|
||||
out_of_date=datetime.datetime.strptime(
|
||||
dump["flag_date"],
|
||||
"%Y-%m-%dT%H:%M:%S.%fZ") if dump["flag_date"] is not None else None,
|
||||
out_of_date=datetime.datetime.fromisoformat(dump["flag_date"]) if dump.get("flag_date") else None,
|
||||
maintainer=next(iter(dump["maintainers"]), None),
|
||||
submitter=None,
|
||||
repository=dump["repo"],
|
||||
@ -208,9 +206,9 @@ class AURPackage:
|
||||
"""
|
||||
identity_mapper: Callable[[Any], Any] = lambda value: value
|
||||
value_mapper: dict[str, Callable[[Any], Any]] = {
|
||||
"out_of_date": lambda value: datetime.datetime.utcfromtimestamp(value) if value is not None else None,
|
||||
"first_submitted": datetime.datetime.utcfromtimestamp,
|
||||
"last_modified": datetime.datetime.utcfromtimestamp,
|
||||
"out_of_date": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC) if value is not None else None,
|
||||
"first_submitted": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC),
|
||||
"last_modified": lambda value: datetime.datetime.fromtimestamp(value, datetime.UTC),
|
||||
}
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
|
62
src/ahriman/models/dependencies.py
Normal file
62
src/ahriman/models/dependencies.py
Normal file
@ -0,0 +1,62 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from dataclasses import dataclass, field, fields
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.util import dataclass_view, filter_json
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Dependencies:
|
||||
"""
|
||||
package paths dependencies
|
||||
|
||||
Attributes:
|
||||
package_base(str): package base
|
||||
paths(dict[Path, list[str]]): map of the paths used by this package to set of packages in which they were found
|
||||
"""
|
||||
|
||||
package_base: str
|
||||
paths: dict[Path, list[str]] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct dependencies from the json dump
|
||||
|
||||
Args:
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: dependencies object
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate json dependencies view
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: json-friendly dictionary
|
||||
"""
|
||||
return dataclass_view(self)
|
165
src/ahriman/models/package_archive.py
Normal file
165
src/ahriman/models/package_archive.py
Normal file
@ -0,0 +1,165 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from dataclasses import dataclass
|
||||
from elftools.elf.dynamic import DynamicSection
|
||||
from elftools.elf.elffile import ELFFile
|
||||
from pathlib import Path
|
||||
from typing import IO
|
||||
|
||||
from ahriman.core.util import walk
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageArchive:
|
||||
"""
|
||||
helper for package archives
|
||||
|
||||
Attributes:
|
||||
package(Package): package descriptor
|
||||
root(Path): path to root filesystem
|
||||
"""
|
||||
|
||||
root: Path
|
||||
package: Package
|
||||
|
||||
@staticmethod
|
||||
def dynamic_needed(binary_path: Path) -> list[str]:
|
||||
"""
|
||||
extract dynamic libraries required by the specified file
|
||||
|
||||
Args:
|
||||
binary_path(Path): path to library, file, etc
|
||||
|
||||
Returns:
|
||||
list[str]: libraries which this file linked dynamically. Returns empty set in case if file is not
|
||||
a binary or no dynamic section has been found
|
||||
"""
|
||||
with binary_path.open("rb") as binary_file:
|
||||
if not PackageArchive.is_elf(binary_file):
|
||||
return []
|
||||
|
||||
elf_file = ELFFile(binary_file) # type: ignore[no-untyped-call]
|
||||
dynamic_section = next(
|
||||
(section for section in elf_file.iter_sections() # type: ignore[no-untyped-call]
|
||||
if isinstance(section, DynamicSection)),
|
||||
None)
|
||||
if dynamic_section is None:
|
||||
return []
|
||||
|
||||
return [
|
||||
tag.needed
|
||||
for tag in dynamic_section.iter_tags() # type: ignore[no-untyped-call]
|
||||
if tag.entry.d_tag == "DT_NEEDED"
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def is_elf(content: IO[bytes]) -> bool:
|
||||
"""
|
||||
check if the content is actually elf file
|
||||
|
||||
Args:
|
||||
content(IO[bytes]): content of the file
|
||||
|
||||
Returns:
|
||||
bool: True in case if file has elf header and False otherwise
|
||||
"""
|
||||
expected = b"\x7fELF"
|
||||
length = len(expected)
|
||||
|
||||
magic_bytes = content.read(length)
|
||||
content.seek(0) # reset reading position
|
||||
|
||||
return magic_bytes == expected
|
||||
|
||||
def depends_on(self) -> Dependencies:
|
||||
"""
|
||||
extract packages and paths which are required for this package
|
||||
|
||||
Returns:
|
||||
Dependencies: map of the package name to set of paths used by this package
|
||||
"""
|
||||
dependencies, roots = self.depends_on_paths()
|
||||
|
||||
result: dict[Path, list[str]] = {}
|
||||
for package, (directories, files) in self.installed_packages().items():
|
||||
if package in self.package.packages:
|
||||
continue # skip package itself
|
||||
|
||||
required_by = [directory for directory in directories if directory in roots]
|
||||
required_by.extend(library for library in files if library.name in dependencies)
|
||||
|
||||
for path in required_by:
|
||||
result.setdefault(path, []).append(package)
|
||||
|
||||
return Dependencies(self.package.base, result)
|
||||
|
||||
def depends_on_paths(self) -> tuple[set[str], set[Path]]:
|
||||
"""
|
||||
extract dependencies from installation
|
||||
|
||||
Returns:
|
||||
tuple[set[str], set[Path]]: tuple of dynamically linked libraries and directory paths
|
||||
"""
|
||||
dependencies = set()
|
||||
roots: set[Path] = set()
|
||||
|
||||
package_dir = self.root / "build" / self.package.base / "pkg"
|
||||
for path in filter(lambda p: p.is_file(), walk(package_dir)):
|
||||
dependencies.update(PackageArchive.dynamic_needed(path))
|
||||
filesystem_path = Path(*path.relative_to(package_dir).parts[1:])
|
||||
roots.update(filesystem_path.parents[:-1]) # last element is always . because paths are relative
|
||||
|
||||
return dependencies, roots
|
||||
|
||||
def installed_packages(self) -> dict[str, tuple[list[Path], list[Path]]]:
|
||||
"""
|
||||
extract list of the installed packages and their content
|
||||
|
||||
Returns:
|
||||
dict[str, tuple[list[Path], list[Path]]]; map of package name to list of directories and files contained
|
||||
by this package
|
||||
"""
|
||||
result = {}
|
||||
|
||||
pacman_local_files = self.root / "var" / "lib" / "pacman" / "local"
|
||||
for path in filter(lambda fn: fn.name == "files", walk(pacman_local_files)):
|
||||
package, *_ = path.parent.name.rsplit("-", 2)
|
||||
|
||||
directories, files = [], []
|
||||
is_files = False
|
||||
for line in path.read_text(encoding="utf8").splitlines():
|
||||
if not line: # skip empty lines
|
||||
continue
|
||||
if line.startswith("%") and line.endswith("%"): # directive started
|
||||
is_files = line == "%FILES%"
|
||||
if not is_files: # not a files directive
|
||||
continue
|
||||
|
||||
entry = Path(line)
|
||||
if line.endswith("/"): # simple check if it is directory
|
||||
directories.append(entry)
|
||||
else:
|
||||
files.append(entry)
|
||||
|
||||
result[package] = directories, files
|
||||
|
||||
return result
|
@ -19,11 +19,11 @@
|
||||
#
|
||||
import shlex
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, fields
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.util import dataclass_view, unquote
|
||||
from ahriman.core.util import dataclass_view, filter_json, unquote
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -90,6 +90,21 @@ class PkgbuildPatch:
|
||||
|
||||
return cls(key, value)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct patch descriptor from the json dump
|
||||
|
||||
Args:
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: patch object
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
def serialize(self) -> str:
|
||||
"""
|
||||
serialize key-value pair into PKGBUILD string. List values will be put inside parentheses. All string
|
||||
|
@ -24,6 +24,7 @@ from collections.abc import Generator
|
||||
from dataclasses import dataclass, field
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from pwd import getpwuid
|
||||
|
||||
from ahriman.core.exceptions import PathError
|
||||
from ahriman.core.log import LazyLogging
|
||||
@ -83,6 +84,17 @@ class RepositoryPaths(LazyLogging):
|
||||
return Path(self.repository_id.architecture) # legacy tree suffix
|
||||
return Path(self.repository_id.name) / self.repository_id.architecture
|
||||
|
||||
@property
|
||||
def build_directory(self) -> Path:
|
||||
"""
|
||||
same as :attr:`chroot`, but exactly build chroot
|
||||
|
||||
Returns:
|
||||
Path: path to directory in which build process is run
|
||||
"""
|
||||
uid, _ = self.owner(self.root)
|
||||
return self.chroot / f"{self.repository_id.name}-{self.repository_id.architecture}" / getpwuid(uid).pw_name
|
||||
|
||||
@property
|
||||
def cache(self) -> Path:
|
||||
"""
|
||||
|
@ -22,6 +22,7 @@ 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.counters_schema import CountersSchema
|
||||
from ahriman.web.schemas.dependencies_schema import DependenciesSchema
|
||||
from ahriman.web.schemas.error_schema import ErrorSchema
|
||||
from ahriman.web.schemas.file_schema import FileSchema
|
||||
from ahriman.web.schemas.info_schema import InfoSchema
|
||||
@ -36,6 +37,7 @@ from ahriman.web.schemas.package_patch_schema import PackagePatchSchema
|
||||
from ahriman.web.schemas.package_properties_schema import PackagePropertiesSchema
|
||||
from ahriman.web.schemas.package_schema import PackageSchema
|
||||
from ahriman.web.schemas.package_status_schema import PackageStatusSchema, PackageStatusSimplifiedSchema
|
||||
from ahriman.web.schemas.package_version_schema import PackageVersionSchema
|
||||
from ahriman.web.schemas.pagination_schema import PaginationSchema
|
||||
from ahriman.web.schemas.patch_name_schema import PatchNameSchema
|
||||
from ahriman.web.schemas.patch_schema import PatchSchema
|
||||
|
35
src/ahriman/web/schemas/dependencies_schema.py
Normal file
35
src/ahriman/web/schemas/dependencies_schema.py
Normal file
@ -0,0 +1,35 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from marshmallow import Schema, fields
|
||||
|
||||
|
||||
class DependenciesSchema(Schema):
|
||||
"""
|
||||
request/response package dependencies schema
|
||||
"""
|
||||
|
||||
package_base = fields.String(metadata={
|
||||
"description": "Package base name",
|
||||
"example": "ahriman",
|
||||
})
|
||||
paths = fields.Dict(
|
||||
keys=fields.String(), values=fields.List(fields.String()), required=True, metadata={
|
||||
"description": "Map of filesystem paths to packages which contain this path",
|
||||
})
|
34
src/ahriman/web/schemas/package_version_schema.py
Normal file
34
src/ahriman/web/schemas/package_version_schema.py
Normal file
@ -0,0 +1,34 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from marshmallow import fields
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||
|
||||
|
||||
class PackageVersionSchema(RepositoryIdSchema):
|
||||
"""
|
||||
request package name schema
|
||||
"""
|
||||
|
||||
version = fields.String(required=True, metadata={
|
||||
"description": "Package version",
|
||||
"example": __version__,
|
||||
})
|
@ -113,7 +113,6 @@ class ChangesView(StatusViewGuard, BaseView):
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
changes = Changes(last_commit_sha, change)
|
||||
repository_id = self.repository_id()
|
||||
self.service(repository_id).database.changes_insert(package_base, changes, repository_id)
|
||||
self.service().client.package_changes_set(package_base, changes)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
66
src/ahriman/web/views/v1/packages/dependencies.py
Normal file
66
src/ahriman/web/views/v1/packages/dependencies.py
Normal file
@ -0,0 +1,66 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import aiohttp_apispec # type: ignore[import-untyped]
|
||||
|
||||
from aiohttp.web import Response, json_response
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, DependenciesSchema, ErrorSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||
|
||||
|
||||
class DependenciesView(StatusViewGuard, BaseView):
|
||||
"""
|
||||
packages dependencies web view
|
||||
|
||||
Attributes:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages/dependencies"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
summary="Get dependencies for all packages",
|
||||
description="Retrieve implicit dependencies for all known packages",
|
||||
responses={
|
||||
200: {"description": "Success response", "schema": DependenciesSchema(many=True)},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get dependencies for all packages
|
||||
|
||||
Returns:
|
||||
Response: 200 with package implicit dependencies on success
|
||||
"""
|
||||
dependencies = self.service().client.package_dependencies_get(None)
|
||||
|
||||
return json_response([dependency.view() for dependency in dependencies])
|
117
src/ahriman/web/views/v1/packages/dependency.py
Normal file
117
src/ahriman/web/views/v1/packages/dependency.py
Normal file
@ -0,0 +1,117 @@
|
||||
#
|
||||
# Copyright (c) 2021-2024 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import aiohttp_apispec # type: ignore[import-untyped]
|
||||
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, DependenciesSchema, ErrorSchema, PackageNameSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||
|
||||
|
||||
class DependencyView(StatusViewGuard, BaseView):
|
||||
"""
|
||||
package dependencies web view
|
||||
|
||||
Attributes:
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages/{package}/dependencies"]
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
summary="Get package dependencies",
|
||||
description="Retrieve package implicit dependencies",
|
||||
responses={
|
||||
200: {"description": "Success response", "schema": DependenciesSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Package base and/or repository are unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [GET_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
async def get(self) -> Response:
|
||||
"""
|
||||
get package dependencies
|
||||
|
||||
Returns:
|
||||
Response: 200 with package implicit dependencies on success
|
||||
|
||||
Raises:
|
||||
HTTPNotFound: if package base is unknown
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
|
||||
try:
|
||||
dependencies = self.service().package_dependencies_get(package_base)
|
||||
except UnknownPackageError:
|
||||
raise HTTPNotFound(reason=f"Package {package_base} is unknown")
|
||||
|
||||
return json_response(dependencies.view())
|
||||
|
||||
@aiohttp_apispec.docs(
|
||||
tags=["Packages"],
|
||||
summary="Update package dependencies",
|
||||
description="Set package implicit dependencies",
|
||||
responses={
|
||||
204: {"description": "Success response"},
|
||||
400: {"description": "Bad data is supplied", "schema": ErrorSchema},
|
||||
401: {"description": "Authorization required", "schema": ErrorSchema},
|
||||
403: {"description": "Access is forbidden", "schema": ErrorSchema},
|
||||
404: {"description": "Repository is unknown", "schema": ErrorSchema},
|
||||
500: {"description": "Internal server error", "schema": ErrorSchema},
|
||||
},
|
||||
security=[{"token": [POST_PERMISSION]}],
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.json_schema(DependenciesSchema)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
insert new package dependencies
|
||||
|
||||
Raises:
|
||||
HTTPBadRequest: if bad data is supplied
|
||||
HTTPNoContent: in case of success response
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
|
||||
try:
|
||||
data = await self.request.json()
|
||||
data["package_base"] = package_base # read from path instead of object
|
||||
dependencies = Dependencies.from_json(data)
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
self.service().client.package_dependencies_set(dependencies)
|
||||
|
||||
raise HTTPNoContent
|
@ -25,8 +25,8 @@ from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.util import pretty_datetime
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, RepositoryIdSchema, \
|
||||
VersionedLogSchema
|
||||
from ahriman.web.schemas import AuthSchema, ErrorSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, \
|
||||
RepositoryIdSchema, VersionedLogSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||
|
||||
@ -60,7 +60,7 @@ class LogsView(StatusViewGuard, BaseView):
|
||||
)
|
||||
@aiohttp_apispec.cookies_schema(AuthSchema)
|
||||
@aiohttp_apispec.match_info_schema(PackageNameSchema)
|
||||
@aiohttp_apispec.querystring_schema(RepositoryIdSchema)
|
||||
@aiohttp_apispec.querystring_schema(PackageVersionSchema)
|
||||
async def delete(self) -> None:
|
||||
"""
|
||||
delete package logs
|
||||
@ -69,7 +69,8 @@ class LogsView(StatusViewGuard, BaseView):
|
||||
HTTPNoContent: on success response
|
||||
"""
|
||||
package_base = self.request.match_info["package"]
|
||||
self.service().logs_remove(package_base, None)
|
||||
version = self.request.query.get("version")
|
||||
self.service().logs_remove(package_base, version)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
||||
|
@ -30,6 +30,7 @@ from ahriman.core.database import SQLite
|
||||
from ahriman.core.distributed import WorkersCache
|
||||
from ahriman.core.exceptions import InitializeError
|
||||
from ahriman.core.spawn import Spawn
|
||||
from ahriman.core.status.client import Client
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.web.apispec import setup_apispec
|
||||
@ -167,7 +168,8 @@ def setup_server(configuration: Configuration, spawner: Spawn, repositories: lis
|
||||
watchers: dict[RepositoryId, Watcher] = {}
|
||||
for repository_id in repositories:
|
||||
application.logger.info("load repository %s", repository_id)
|
||||
watchers[repository_id] = Watcher(repository_id, database)
|
||||
client = Client.load(repository_id, configuration, database, report=False) # explicitly load local client
|
||||
watchers[repository_id] = Watcher(client)
|
||||
application[WatcherKey] = watchers
|
||||
# workers cache
|
||||
application[WorkersKey] = WorkersCache(configuration)
|
||||
|
@ -131,25 +131,6 @@ def test_sign_skip(application_repository: ApplicationRepository, package_ahrima
|
||||
application_repository.sign([])
|
||||
|
||||
|
||||
def test_sign_specific(application_repository: ApplicationRepository, package_ahriman: Package,
|
||||
package_python_schedule: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sign only specified packages
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
sign_package_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_package")
|
||||
sign_repository_mock = mocker.patch("ahriman.core.sign.gpg.GPG.process_sign_repository")
|
||||
on_result_mock = mocker.patch(
|
||||
"ahriman.application.application.application_repository.ApplicationRepository.on_result")
|
||||
|
||||
filename = package_ahriman.packages[package_ahriman.base].filepath
|
||||
application_repository.sign([package_ahriman.base])
|
||||
sign_package_mock.assert_called_once_with(filename, None)
|
||||
sign_repository_mock.assert_called_once_with(application_repository.repository.repo.repo_path)
|
||||
on_result_mock.assert_called_once_with(Result())
|
||||
|
||||
|
||||
def test_unknown_no_aur(application_repository: ApplicationRepository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
@ -239,11 +220,13 @@ def test_updates_all(application_repository: ApplicationRepository, package_ahri
|
||||
return_value=[package_ahriman])
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=True, local=True, manual=True, vcs=True)
|
||||
application_repository.updates([], aur=True, local=True, manual=True, vcs=True, check_files=True)
|
||||
updates_aur_mock.assert_called_once_with([], vcs=True)
|
||||
updates_local_mock.assert_called_once_with(vcs=True)
|
||||
updates_manual_mock.assert_called_once_with()
|
||||
updates_deps_mock.assert_called_once_with([])
|
||||
|
||||
|
||||
def test_updates_disabled(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
@ -253,11 +236,13 @@ def test_updates_disabled(application_repository: ApplicationRepository, mocker:
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=False, local=False, manual=False, vcs=True)
|
||||
application_repository.updates([], aur=False, local=False, manual=False, vcs=True, check_files=False)
|
||||
updates_aur_mock.assert_not_called()
|
||||
updates_local_mock.assert_not_called()
|
||||
updates_manual_mock.assert_not_called()
|
||||
updates_deps_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_updates_no_aur(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
@ -267,11 +252,13 @@ def test_updates_no_aur(application_repository: ApplicationRepository, mocker: M
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=False, local=True, manual=True, vcs=True)
|
||||
application_repository.updates([], aur=False, local=True, manual=True, vcs=True, check_files=True)
|
||||
updates_aur_mock.assert_not_called()
|
||||
updates_local_mock.assert_called_once_with(vcs=True)
|
||||
updates_manual_mock.assert_called_once_with()
|
||||
updates_deps_mock.assert_called_once_with([])
|
||||
|
||||
|
||||
def test_updates_no_local(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
@ -281,11 +268,13 @@ def test_updates_no_local(application_repository: ApplicationRepository, mocker:
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=True, local=False, manual=True, vcs=True)
|
||||
application_repository.updates([], aur=True, local=False, manual=True, vcs=True, check_files=True)
|
||||
updates_aur_mock.assert_called_once_with([], vcs=True)
|
||||
updates_local_mock.assert_not_called()
|
||||
updates_manual_mock.assert_called_once_with()
|
||||
updates_deps_mock.assert_called_once_with([])
|
||||
|
||||
|
||||
def test_updates_no_manual(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
@ -295,11 +284,13 @@ def test_updates_no_manual(application_repository: ApplicationRepository, mocker
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=True, local=True, manual=False, vcs=True)
|
||||
application_repository.updates([], aur=True, local=True, manual=False, vcs=True, check_files=True)
|
||||
updates_aur_mock.assert_called_once_with([], vcs=True)
|
||||
updates_local_mock.assert_called_once_with(vcs=True)
|
||||
updates_manual_mock.assert_not_called()
|
||||
updates_deps_mock.assert_called_once_with([])
|
||||
|
||||
|
||||
def test_updates_no_vcs(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
@ -309,11 +300,29 @@ def test_updates_no_vcs(application_repository: ApplicationRepository, mocker: M
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=True, local=True, manual=True, vcs=False)
|
||||
application_repository.updates([], aur=True, local=True, manual=True, vcs=False, check_files=True)
|
||||
updates_aur_mock.assert_called_once_with([], vcs=False)
|
||||
updates_local_mock.assert_called_once_with(vcs=False)
|
||||
updates_manual_mock.assert_called_once_with()
|
||||
updates_deps_mock.assert_called_once_with([])
|
||||
|
||||
|
||||
def test_updates_no_check_files(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must get updates without checking broken links
|
||||
"""
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates([], aur=True, local=True, manual=True, vcs=True, check_files=False)
|
||||
updates_aur_mock.assert_called_once_with([], vcs=True)
|
||||
updates_local_mock.assert_called_once_with(vcs=True)
|
||||
updates_manual_mock.assert_called_once_with()
|
||||
updates_deps_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_updates_with_filter(application_repository: ApplicationRepository, mocker: MockerFixture) -> None:
|
||||
@ -323,8 +332,10 @@ def test_updates_with_filter(application_repository: ApplicationRepository, mock
|
||||
updates_aur_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_aur")
|
||||
updates_local_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_local")
|
||||
updates_manual_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_manual")
|
||||
updates_deps_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.updates_dependencies")
|
||||
|
||||
application_repository.updates(["filter"], aur=True, local=True, manual=True, vcs=True)
|
||||
application_repository.updates(["filter"], aur=True, local=True, manual=True, vcs=True, check_files=True)
|
||||
updates_aur_mock.assert_called_once_with(["filter"], vcs=True)
|
||||
updates_local_mock.assert_called_once_with(vcs=True)
|
||||
updates_manual_mock.assert_called_once_with()
|
||||
updates_deps_mock.assert_called_once_with(["filter"])
|
||||
|
@ -89,7 +89,8 @@ def test_run_with_updates(args: argparse.Namespace, configuration: Configuration
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Add.run(args, repository_id, configuration, report=False)
|
||||
updates_mock.assert_called_once_with(args.package, aur=False, local=False, manual=True, vcs=False)
|
||||
updates_mock.assert_called_once_with(args.package,
|
||||
aur=False, local=False, manual=True, vcs=False, check_files=False)
|
||||
application_mock.assert_called_once_with([package_ahriman],
|
||||
Packagers(args.username, {package_ahriman.base: "packager"}),
|
||||
bump_pkgrel=args.increment)
|
||||
|
@ -31,7 +31,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
|
||||
mocker.patch("ahriman.application.handlers.Backup.get_paths", return_value=[Path("path")])
|
||||
tarfile = MagicMock()
|
||||
add_mock = tarfile.__enter__.return_value = MagicMock()
|
||||
mocker.patch("tarfile.TarFile.__new__", return_value=tarfile)
|
||||
mocker.patch("ahriman.application.handlers.backup.tarfile.open", return_value=tarfile)
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Backup.run(args, repository_id, configuration, report=False)
|
||||
@ -45,7 +45,7 @@ def test_get_paths(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
# gnupg export mock
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
mocker.patch.object(RepositoryPaths, "root_owner", (42, 42))
|
||||
getpwuid_mock = mocker.patch("pwd.getpwuid", return_value=MagicMock())
|
||||
getpwuid_mock = mocker.patch("ahriman.application.handlers.backup.getpwuid", return_value=MagicMock())
|
||||
# well database does not exist so we override it
|
||||
database_mock = mocker.patch("ahriman.core.database.SQLite.database_path", return_value=configuration.path)
|
||||
|
||||
|
@ -30,7 +30,7 @@ def test_run(args: argparse.Namespace, configuration: Configuration, mocker: Moc
|
||||
args = _default_args(args)
|
||||
tarfile = MagicMock()
|
||||
extract_mock = tarfile.__enter__.return_value = MagicMock()
|
||||
mocker.patch("tarfile.TarFile.__new__", return_value=tarfile)
|
||||
mocker.patch("ahriman.application.handlers.restore.tarfile.open", return_value=tarfile)
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Restore.run(args, repository_id, configuration, report=False)
|
||||
|
@ -25,6 +25,7 @@ def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
args.aur = True
|
||||
args.changes = True
|
||||
args.check_files = True
|
||||
args.package = []
|
||||
args.dependencies = True
|
||||
args.dry_run = False
|
||||
@ -61,7 +62,8 @@ def test_run(args: argparse.Namespace, package_ahriman: Package, configuration:
|
||||
application_mock.assert_called_once_with([package_ahriman],
|
||||
Packagers(args.username, {package_ahriman.base: "packager"}),
|
||||
bump_pkgrel=args.increment)
|
||||
updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs)
|
||||
updates_mock.assert_called_once_with(
|
||||
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, check_files=args.check_files)
|
||||
changes_mock.assert_not_called()
|
||||
dependencies_mock.assert_called_once_with([package_ahriman], process_dependencies=args.dependencies)
|
||||
check_mock.assert_called_once_with(False, False)
|
||||
@ -122,7 +124,8 @@ def test_run_dry_run(args: argparse.Namespace, package_ahriman: Package, configu
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Update.run(args, repository_id, configuration, report=False)
|
||||
updates_mock.assert_called_once_with(args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs)
|
||||
updates_mock.assert_called_once_with(
|
||||
args.package, aur=args.aur, local=args.local, manual=args.manual, vcs=args.vcs, check_files=args.check_files)
|
||||
application_mock.assert_not_called()
|
||||
changes_mock.assert_called_once_with([package_ahriman])
|
||||
check_mock.assert_called_once_with(False, pytest.helpers.anyvar(int))
|
||||
|
@ -133,8 +133,8 @@ def aur_package_ahriman() -> AURPackage:
|
||||
description="ArcH linux ReposItory MANager",
|
||||
num_votes=0,
|
||||
popularity=0,
|
||||
first_submitted=datetime.datetime.utcfromtimestamp(1618008285),
|
||||
last_modified=datetime.datetime.utcfromtimestamp(1673826351),
|
||||
first_submitted=datetime.datetime.fromtimestamp(1618008285, datetime.UTC),
|
||||
last_modified=datetime.datetime.fromtimestamp(1673826351, datetime.UTC),
|
||||
url_path="/cgit/aur.git/snapshot/ahriman.tar.gz",
|
||||
url="https://github.com/arcan1s/ahriman",
|
||||
out_of_date=None,
|
||||
@ -200,8 +200,8 @@ def aur_package_akonadi() -> AURPackage:
|
||||
description="PIM layer, which provides an asynchronous API to access all kind of PIM data",
|
||||
num_votes=0,
|
||||
popularity=0.0,
|
||||
first_submitted=datetime.datetime.utcfromtimestamp(0),
|
||||
last_modified=datetime.datetime.utcfromtimestamp(1646555990.610),
|
||||
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
|
||||
last_modified=datetime.datetime.fromtimestamp(1646555990.610, datetime.UTC),
|
||||
url_path="",
|
||||
url="https://kontact.kde.org",
|
||||
out_of_date=None,
|
||||
@ -451,6 +451,7 @@ def passwd() -> MagicMock:
|
||||
"""
|
||||
passwd = MagicMock()
|
||||
passwd.pw_dir = "home"
|
||||
passwd.pw_name = "ahriman"
|
||||
return passwd
|
||||
|
||||
|
||||
|
21
tests/ahriman/core/alpm/conftest.py
Normal file
21
tests/ahriman/core/alpm/conftest.py
Normal file
@ -0,0 +1,21 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pacman_database(configuration: Configuration, pacman: Pacman) -> PacmanDatabase:
|
||||
"""
|
||||
database sync fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration test instance
|
||||
pacman(Pacman): pacman test instance
|
||||
|
||||
Returns:
|
||||
DatabaseSync: database sync test instance
|
||||
"""
|
||||
database = next(iter(pacman.handle.get_syncdbs()))
|
||||
return PacmanDatabase(database, configuration)
|
@ -14,7 +14,7 @@ def test_package_info(official_syncdb: OfficialSyncdb, aur_package_akonadi: AURP
|
||||
must return package info from the database
|
||||
"""
|
||||
mocker.patch("ahriman.models.aur_package.AURPackage.from_pacman", return_value=aur_package_akonadi)
|
||||
get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.package_get", return_value=[aur_package_akonadi])
|
||||
get_mock = mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[aur_package_akonadi])
|
||||
|
||||
package = official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman)
|
||||
get_mock.assert_called_once_with(aur_package_akonadi.name)
|
||||
@ -26,7 +26,7 @@ def test_package_info_no_pacman(official_syncdb: OfficialSyncdb, aur_package_ako
|
||||
"""
|
||||
must raise UnknownPackageError if no pacman set
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.package_get", return_value=[aur_package_akonadi])
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[aur_package_akonadi])
|
||||
|
||||
with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name):
|
||||
official_syncdb.package_info(aur_package_akonadi.name, pacman=None)
|
||||
@ -37,6 +37,6 @@ def test_package_info_not_found(official_syncdb: OfficialSyncdb, aur_package_ako
|
||||
"""
|
||||
must raise UnknownPackage exception in case if no package was found
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.package_get", return_value=[])
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.package", return_value=[])
|
||||
with pytest.raises(UnknownPackageError, match=aur_package_akonadi.name):
|
||||
assert official_syncdb.package_info(aur_package_akonadi.name, pacman=pacman)
|
||||
|
@ -1,13 +1,14 @@
|
||||
import pytest
|
||||
import tarfile
|
||||
|
||||
from pathlib import Path
|
||||
from pyalpm import error as PyalpmError
|
||||
from pytest_mock import MockerFixture
|
||||
from tempfile import TemporaryDirectory
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
@ -48,7 +49,7 @@ def test_init_with_local_cache_forced(configuration: Configuration, mocker: Mock
|
||||
sync_mock.assert_called_once_with(pytest.helpers.anyvar(int), force=True)
|
||||
|
||||
|
||||
def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
def test_database_copy(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must copy database from root
|
||||
"""
|
||||
@ -62,13 +63,13 @@ def test_database_copy(pacman: Pacman, repository_paths: RepositoryPaths, mocker
|
||||
copy_mock = mocker.patch("shutil.copy")
|
||||
chown_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.chown")
|
||||
|
||||
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True)
|
||||
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
|
||||
mkdir_mock.assert_called_once_with(mode=0o755, exist_ok=True)
|
||||
copy_mock.assert_called_once_with(path / "sync" / "core.db", dst_path)
|
||||
chown_mock.assert_called_once_with(dst_path)
|
||||
|
||||
|
||||
def test_database_copy_skip(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
def test_database_copy_skip(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do not copy database from root if local cache is disabled
|
||||
"""
|
||||
@ -79,11 +80,11 @@ def test_database_copy_skip(pacman: Pacman, repository_paths: RepositoryPaths, m
|
||||
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
|
||||
copy_mock = mocker.patch("shutil.copy")
|
||||
|
||||
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=False)
|
||||
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=False)
|
||||
copy_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_database_copy_no_directory(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
def test_database_copy_no_directory(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do not copy database if local cache already exists
|
||||
"""
|
||||
@ -94,11 +95,11 @@ def test_database_copy_no_directory(pacman: Pacman, repository_paths: Repository
|
||||
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=lambda p: p.is_relative_to(path))
|
||||
copy_mock = mocker.patch("shutil.copy")
|
||||
|
||||
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True)
|
||||
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
|
||||
copy_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_database_copy_no_root_file(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
def test_database_copy_no_root_file(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do not copy database if no repository file exists in filesystem
|
||||
"""
|
||||
@ -109,11 +110,11 @@ def test_database_copy_no_root_file(pacman: Pacman, repository_paths: Repository
|
||||
mocker.patch("pathlib.Path.is_file", return_value=False)
|
||||
copy_mock = mocker.patch("shutil.copy")
|
||||
|
||||
pacman.database_copy(pacman.handle, database, path, repository_paths, use_ahriman_cache=True)
|
||||
pacman.database_copy(pacman.handle, database, path, use_ahriman_cache=True)
|
||||
copy_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_database_copy_database_exist(pacman: Pacman, repository_paths: RepositoryPaths, mocker: MockerFixture) -> None:
|
||||
def test_database_copy_database_exist(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do not copy database if local cache already exists
|
||||
"""
|
||||
@ -123,7 +124,7 @@ def test_database_copy_database_exist(pacman: Pacman, repository_paths: Reposito
|
||||
mocker.patch("pathlib.Path.is_file", return_value=True)
|
||||
copy_mock = mocker.patch("shutil.copy")
|
||||
|
||||
pacman.database_copy(pacman.handle, database, Path("root"), repository_paths, use_ahriman_cache=True)
|
||||
pacman.database_copy(pacman.handle, database, Path("root"), use_ahriman_cache=True)
|
||||
copy_mock.assert_not_called()
|
||||
|
||||
|
||||
@ -131,71 +132,133 @@ def test_database_init(pacman: Pacman, configuration: Configuration) -> None:
|
||||
"""
|
||||
must init database with settings
|
||||
"""
|
||||
mirror = configuration.get("alpm", "mirror")
|
||||
database = pacman.database_init(pacman.handle, "testing", mirror, "x86_64")
|
||||
database = pacman.database_init(pacman.handle, "testing", "x86_64")
|
||||
assert database.servers == ["https://geo.mirror.pkgbuild.com/testing/os/x86_64"]
|
||||
|
||||
|
||||
def test_database_sync(pacman: Pacman) -> None:
|
||||
def test_database_init_local(pacman: Pacman, configuration: Configuration) -> None:
|
||||
"""
|
||||
must set file protocol for local databases
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
database = pacman.database_init(MagicMock(), repository_id.name, repository_id.architecture)
|
||||
assert database.servers == [f"file://{configuration.repository_paths.repository}"]
|
||||
|
||||
|
||||
def test_database_sync(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sync databases
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
core_mock = MagicMock()
|
||||
extra_mock = MagicMock()
|
||||
transaction_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [core_mock, extra_mock]
|
||||
handle_mock.get_syncdbs.return_value = [1, 2]
|
||||
handle_mock.init_transaction.return_value = transaction_mock
|
||||
pacman.handle = handle_mock
|
||||
|
||||
pacman.database_sync(pacman.handle, force=False)
|
||||
sync_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync")
|
||||
|
||||
pacman.database_sync(handle_mock, force=False)
|
||||
handle_mock.init_transaction.assert_called_once_with()
|
||||
core_mock.update.assert_called_once_with(False)
|
||||
extra_mock.update.assert_called_once_with(False)
|
||||
sync_mock.assert_has_calls([MockCall(force=False), MockCall(force=False)])
|
||||
transaction_mock.release.assert_called_once_with()
|
||||
|
||||
|
||||
def test_database_sync_failed(pacman: Pacman) -> None:
|
||||
"""
|
||||
must sync databases even if there was exception
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
core_mock = MagicMock()
|
||||
core_mock.update.side_effect = PyalpmError()
|
||||
extra_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [core_mock, extra_mock]
|
||||
pacman.handle = handle_mock
|
||||
|
||||
pacman.database_sync(pacman.handle, force=False)
|
||||
extra_mock.update.assert_called_once_with(False)
|
||||
|
||||
|
||||
def test_database_sync_forced(pacman: Pacman) -> None:
|
||||
def test_database_sync_forced(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sync databases with force flag
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
core_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [core_mock]
|
||||
handle_mock.get_syncdbs.return_value = [1]
|
||||
|
||||
sync_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync")
|
||||
|
||||
pacman.database_sync(handle_mock, force=True)
|
||||
sync_mock.assert_called_once_with(force=True)
|
||||
|
||||
|
||||
def test_files(pacman: Pacman, package_ahriman: Package, mocker: MockerFixture, resource_path_root: Path) -> None:
|
||||
"""
|
||||
must load files from databases
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [MagicMock()]
|
||||
pacman.handle = handle_mock
|
||||
tarball = resource_path_root / "core" / "arcanisrepo.files.tar.gz"
|
||||
|
||||
mocker.patch("pathlib.Path.is_file", return_value=True)
|
||||
open_mock = mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=tarfile.open(tarball, "r:gz"))
|
||||
|
||||
files = pacman.files()
|
||||
assert len(files) == 2
|
||||
assert package_ahriman.base in files
|
||||
assert Path("usr/bin/ahriman") in files[package_ahriman.base]
|
||||
open_mock.assert_called_once_with(pytest.helpers.anyvar(int), "r:gz")
|
||||
|
||||
|
||||
def test_files_package(pacman: Pacman, package_ahriman: Package, mocker: MockerFixture,
|
||||
resource_path_root: Path) -> None:
|
||||
"""
|
||||
must load files only for the specified package
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [MagicMock()]
|
||||
pacman.handle = handle_mock
|
||||
|
||||
pacman.database_sync(pacman.handle, force=True)
|
||||
handle_mock.init_transaction.assert_called_once_with()
|
||||
core_mock.update.assert_called_once_with(True)
|
||||
tarball = resource_path_root / "core" / "arcanisrepo.files.tar.gz"
|
||||
|
||||
mocker.patch("pathlib.Path.is_file", return_value=True)
|
||||
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=tarfile.open(tarball, "r:gz"))
|
||||
|
||||
files = pacman.files(package_ahriman.base)
|
||||
assert len(files) == 1
|
||||
assert package_ahriman.base in files
|
||||
|
||||
|
||||
def test_package_get(pacman: Pacman) -> None:
|
||||
def test_files_skip(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return empty list if no database found
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [MagicMock()]
|
||||
pacman.handle = handle_mock
|
||||
|
||||
mocker.patch("pathlib.Path.is_file", return_value=False)
|
||||
|
||||
assert not pacman.files()
|
||||
|
||||
|
||||
def test_files_no_content(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip package if no content can be loaded
|
||||
"""
|
||||
handle_mock = MagicMock()
|
||||
handle_mock.get_syncdbs.return_value = [MagicMock()]
|
||||
pacman.handle = handle_mock
|
||||
|
||||
tar_mock = MagicMock()
|
||||
tar_mock.getmembers.return_value = [MagicMock()]
|
||||
tar_mock.extractfile.return_value = None
|
||||
|
||||
open_mock = MagicMock()
|
||||
open_mock.__enter__.return_value = tar_mock
|
||||
|
||||
mocker.patch("pathlib.Path.is_file", return_value=True)
|
||||
mocker.patch("ahriman.core.alpm.pacman.tarfile.open", return_value=open_mock)
|
||||
|
||||
assert not pacman.files()
|
||||
|
||||
|
||||
def test_package(pacman: Pacman) -> None:
|
||||
"""
|
||||
must retrieve package
|
||||
"""
|
||||
assert list(pacman.package_get("pacman"))
|
||||
assert list(pacman.package("pacman"))
|
||||
|
||||
|
||||
def test_package_get_empty(pacman: Pacman) -> None:
|
||||
def test_package_empty(pacman: Pacman) -> None:
|
||||
"""
|
||||
must return empty packages list without exception
|
||||
"""
|
||||
assert not list(pacman.package_get("some-random-name"))
|
||||
assert not list(pacman.package("some-random-name"))
|
||||
|
||||
|
||||
def test_packages(pacman: Pacman) -> None:
|
||||
|
203
tests/ahriman/core/alpm/test_pacman_database.py
Normal file
203
tests/ahriman/core/alpm/test_pacman_database.py
Normal file
@ -0,0 +1,203 @@
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
|
||||
from ahriman.core.alpm.pacman_database import PacmanDatabase
|
||||
from ahriman.core.exceptions import PacmanError
|
||||
|
||||
|
||||
def test_copy(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must copy loca database file
|
||||
"""
|
||||
copy_mock = mocker.patch("shutil.copy")
|
||||
pacman_database.copy(Path("remote"), Path("local"))
|
||||
copy_mock.assert_called_once_with(Path("remote"), Path("local"))
|
||||
|
||||
|
||||
def test_download(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must download database by remote url
|
||||
"""
|
||||
response_obj = MagicMock()
|
||||
response_obj.headers = {pacman_database.LAST_MODIFIED_HEADER: "Fri, 09 Feb 2024 00:25:55 GMT"}
|
||||
response_obj.iter_content.return_value = ["chunk".encode("utf8")]
|
||||
|
||||
path = Path("local")
|
||||
url = "url"
|
||||
|
||||
file_mock = MagicMock()
|
||||
request_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request",
|
||||
return_value=response_obj)
|
||||
open_mock = mocker.patch("pathlib.Path.open")
|
||||
open_mock.return_value.__enter__.return_value = file_mock
|
||||
mtime_mock = mocker.patch("os.utime")
|
||||
|
||||
pacman_database.download(url, path)
|
||||
request_mock.assert_called_once_with("GET", url, stream=True)
|
||||
open_mock.assert_called_once_with("wb")
|
||||
file_mock.write.assert_called_once_with("chunk".encode("utf8"))
|
||||
mtime_mock.assert_called_once_with(path, (1707438355.0, 1707438355.0))
|
||||
|
||||
|
||||
def test_download_no_header(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise exception in case if no last modified head found
|
||||
"""
|
||||
response_obj = MagicMock()
|
||||
response_obj.headers = {}
|
||||
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request", return_value=response_obj)
|
||||
|
||||
with pytest.raises(PacmanError):
|
||||
pacman_database.download("url", Path("local"))
|
||||
|
||||
|
||||
def test_is_outdated(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly check if file is outdated
|
||||
"""
|
||||
response_obj = MagicMock()
|
||||
response_obj.headers = {pacman_database.LAST_MODIFIED_HEADER: "Fri, 09 Feb 2024 00:25:55 GMT"}
|
||||
stat_mock = MagicMock()
|
||||
stat_mock.st_mtime = 1707438354
|
||||
|
||||
path = Path("local")
|
||||
url = "url"
|
||||
|
||||
mocker.patch("pathlib.Path.is_file", return_value=True)
|
||||
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request", return_value=response_obj)
|
||||
mocker.patch("pathlib.Path.stat", return_value=stat_mock)
|
||||
|
||||
assert pacman_database.is_outdated(url, path)
|
||||
|
||||
stat_mock.st_mtime += 1
|
||||
assert not pacman_database.is_outdated(url, path)
|
||||
|
||||
stat_mock.st_mtime += 1
|
||||
assert not pacman_database.is_outdated(url, path)
|
||||
|
||||
|
||||
def test_is_outdated_not_a_file(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must mark as outdated if no file was found
|
||||
"""
|
||||
mocker.patch("pathlib.Path.is_file", return_value=False)
|
||||
assert pacman_database.is_outdated("url", Path("local"))
|
||||
|
||||
|
||||
def test_is_outdated_no_header(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise exception in case if no last modified head found during timestamp check
|
||||
"""
|
||||
response_obj = MagicMock()
|
||||
response_obj.headers = {}
|
||||
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.make_request", return_value=response_obj)
|
||||
mocker.patch("pathlib.Path.is_file", return_value=True)
|
||||
|
||||
with pytest.raises(PacmanError):
|
||||
pacman_database.is_outdated("url", Path("local"))
|
||||
|
||||
|
||||
def test_sync(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sync database
|
||||
"""
|
||||
sync_db_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_packages")
|
||||
sync_files_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_files")
|
||||
|
||||
pacman_database.sync(force=True)
|
||||
pacman_database.sync(force=False)
|
||||
sync_db_mock.assert_has_calls([MockCall(force=True), MockCall(force=False)])
|
||||
sync_files_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_with_files(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sync database and files
|
||||
"""
|
||||
sync_db_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_packages")
|
||||
sync_files_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_files")
|
||||
pacman_database.sync_files_database = True
|
||||
|
||||
pacman_database.sync(force=True)
|
||||
pacman_database.sync(force=False)
|
||||
sync_db_mock.assert_has_calls([MockCall(force=True), MockCall(force=False)])
|
||||
sync_files_mock.assert_has_calls([MockCall(force=True), MockCall(force=False)])
|
||||
|
||||
|
||||
def test_sync_exception(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress all exceptions on failure
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.sync_packages", side_effect=Exception())
|
||||
pacman_database.sync(force=True)
|
||||
|
||||
|
||||
def test_sync_files(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sync files database
|
||||
"""
|
||||
outdated_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.is_outdated", return_value=True)
|
||||
download_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.download")
|
||||
|
||||
pacman_database.sync_files(force=False)
|
||||
outdated_mock.assert_called_once_with(
|
||||
"https://geo.mirror.pkgbuild.com/core/os/x86_64/core.files.tar.gz", pytest.helpers.anyvar(int))
|
||||
download_mock.assert_called_once_with(
|
||||
"https://geo.mirror.pkgbuild.com/core/os/x86_64/core.files.tar.gz", pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_sync_files_not_outdated(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip files sync if up-to-date
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.is_outdated", return_value=False)
|
||||
download_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.download")
|
||||
|
||||
pacman_database.sync_files(force=False)
|
||||
download_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_sync_files_force(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must sync up-to-date files if force flag is set
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.is_outdated", return_value=False)
|
||||
download_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.download")
|
||||
|
||||
pacman_database.sync_files(force=True)
|
||||
download_mock.assert_called_once_with(
|
||||
"https://geo.mirror.pkgbuild.com/core/os/x86_64/core.files.tar.gz", pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_sync_files_local(pacman_database: PacmanDatabase, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must copy local files instead of downloading them
|
||||
"""
|
||||
pacman_database.database.servers = ["file:///var"]
|
||||
copy_mock = mocker.patch("ahriman.core.alpm.pacman_database.PacmanDatabase.copy")
|
||||
|
||||
pacman_database.sync_files(force=False)
|
||||
copy_mock.assert_called_once_with(Path("/var/core.files.tar.gz"), pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_sync_files_unknown_source(pacman_database: PacmanDatabase) -> None:
|
||||
"""
|
||||
must raise an exception in case if server scheme is unsupported
|
||||
"""
|
||||
pacman_database.database.servers = ["some random string"]
|
||||
with pytest.raises(PacmanError):
|
||||
pacman_database.sync_files(force=False)
|
||||
|
||||
|
||||
def test_sync_packages(pacman_database: PacmanDatabase) -> None:
|
||||
"""
|
||||
must sync packages by using pyalpm method
|
||||
"""
|
||||
pacman_database.database = MagicMock()
|
||||
|
||||
pacman_database.sync_packages(force=True)
|
||||
pacman_database.sync_packages(force=False)
|
||||
pacman_database.database.update.assert_has_calls([MockCall(True), MockCall(False)])
|
@ -26,13 +26,28 @@ def test_repo_add(repo: Repo, mocker: MockerFixture) -> None:
|
||||
|
||||
def test_repo_init(repo: Repo, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call repo-add with empty package list on repo initializing
|
||||
must create empty database files
|
||||
"""
|
||||
check_output_mock = mocker.patch("ahriman.core.alpm.repo.check_output")
|
||||
mocker.patch("pathlib.Path.exists", return_value=False)
|
||||
touch_mock = mocker.patch("pathlib.Path.touch")
|
||||
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||
|
||||
repo.init()
|
||||
check_output_mock.assert_called_once() # it will be checked later
|
||||
assert check_output_mock.call_args[0][0] == "repo-add"
|
||||
touch_mock.assert_called_once_with(exist_ok=True)
|
||||
symlink_mock.assert_called_once_with(repo.repo_path)
|
||||
|
||||
|
||||
def test_repo_init_skip(repo: Repo, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must do not create files if database already exists
|
||||
"""
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
touch_mock = mocker.patch("pathlib.Path.touch")
|
||||
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||
|
||||
repo.init()
|
||||
touch_mock.assert_not_called()
|
||||
symlink_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_repo_remove(repo: Repo, mocker: MockerFixture) -> None:
|
||||
|
@ -1,5 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.database import SQLite
|
||||
@ -9,9 +12,83 @@ def test_build(task_ahriman: Task, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must build package
|
||||
"""
|
||||
local = Path("local")
|
||||
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
|
||||
task_ahriman.build(Path("ahriman"))
|
||||
check_output_mock.assert_called()
|
||||
|
||||
task_ahriman.build(local)
|
||||
check_output_mock.assert_has_calls([
|
||||
MockCall(
|
||||
"extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck",
|
||||
exception=pytest.helpers.anyvar(int),
|
||||
cwd=local,
|
||||
logger=task_ahriman.logger,
|
||||
user=task_ahriman.uid,
|
||||
environment={},
|
||||
),
|
||||
MockCall(
|
||||
"makepkg", "--packagelist",
|
||||
exception=pytest.helpers.anyvar(int),
|
||||
cwd=local,
|
||||
logger=task_ahriman.logger,
|
||||
environment={},
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
def test_build_environment(task_ahriman: Task, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must build package with environment variables set
|
||||
"""
|
||||
local = Path("local")
|
||||
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
|
||||
environment = {"variable": "value"}
|
||||
|
||||
task_ahriman.build(local, **environment, empty=None)
|
||||
check_output_mock.assert_has_calls([
|
||||
MockCall(
|
||||
"extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck",
|
||||
exception=pytest.helpers.anyvar(int),
|
||||
cwd=local,
|
||||
logger=task_ahriman.logger,
|
||||
user=task_ahriman.uid,
|
||||
environment=environment,
|
||||
),
|
||||
MockCall(
|
||||
"makepkg", "--packagelist",
|
||||
exception=pytest.helpers.anyvar(int),
|
||||
cwd=local,
|
||||
logger=task_ahriman.logger,
|
||||
environment=environment,
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
def test_build_no_debug(task_ahriman: Task, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter debug packages from result
|
||||
"""
|
||||
local = Path("local")
|
||||
check_output_mock = mocker.patch("ahriman.core.build_tools.task.check_output")
|
||||
task_ahriman.include_debug_packages = False
|
||||
|
||||
task_ahriman.build(local)
|
||||
check_output_mock.assert_has_calls([
|
||||
MockCall(
|
||||
"extra-x86_64-build", "-r", str(task_ahriman.paths.chroot), "--", "--", "--skippgpcheck",
|
||||
exception=pytest.helpers.anyvar(int),
|
||||
cwd=local,
|
||||
logger=task_ahriman.logger,
|
||||
user=task_ahriman.uid,
|
||||
environment={},
|
||||
),
|
||||
MockCall(
|
||||
"makepkg", "--packagelist", "OPTIONS=(!debug)",
|
||||
exception=pytest.helpers.anyvar(int),
|
||||
cwd=local,
|
||||
logger=task_ahriman.logger,
|
||||
environment={},
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
def test_init(task_ahriman: Task, database: SQLite, mocker: MockerFixture) -> None:
|
||||
|
@ -0,0 +1,8 @@
|
||||
from ahriman.core.database.migrations.m013_dependencies import steps
|
||||
|
||||
|
||||
def test_migration_dependencies() -> None:
|
||||
"""
|
||||
migration must not be empty
|
||||
"""
|
||||
assert steps
|
@ -0,0 +1,61 @@
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
def test_dependencies_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get dependencies
|
||||
"""
|
||||
dependencies = Dependencies(package_ahriman.base, {Path("usr/lib/python3.11/site-packages"): ["python"]})
|
||||
database.dependencies_insert(dependencies)
|
||||
assert database.dependencies_get(package_ahriman.base) == [dependencies]
|
||||
|
||||
dependencies2 = Dependencies(package_ahriman.base, {Path("usr/lib/python3.11/site-packages"): ["python3"]})
|
||||
database.dependencies_insert(dependencies2, RepositoryId("i686", database._repository_id.name))
|
||||
assert database.dependencies_get() == [dependencies]
|
||||
assert database.dependencies_get(package_ahriman.base) == [dependencies]
|
||||
assert database.dependencies_get(
|
||||
package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == [dependencies2]
|
||||
|
||||
|
||||
def test_dependencies_insert_remove(database: SQLite, package_ahriman: Package,
|
||||
package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must remove dependencies for the package
|
||||
"""
|
||||
dependencies1 = Dependencies(package_ahriman.base, {Path("usr"): ["python"]})
|
||||
database.dependencies_insert(dependencies1)
|
||||
dependencies2 = Dependencies(package_python_schedule.base, {Path("usr"): ["filesystem"]})
|
||||
database.dependencies_insert(dependencies2)
|
||||
dependencies3 = Dependencies(package_ahriman.base, {Path("usr"): ["python3"]})
|
||||
database.dependencies_insert(dependencies3, RepositoryId("i686", database._repository_id.name))
|
||||
|
||||
assert database.dependencies_get() == [dependencies1, dependencies2]
|
||||
|
||||
database.dependencies_remove(package_ahriman.base)
|
||||
assert database.dependencies_get(package_ahriman.base) == []
|
||||
assert database.dependencies_get(package_python_schedule.base) == [dependencies2]
|
||||
|
||||
# insert null
|
||||
database.dependencies_remove(package_ahriman.base, RepositoryId("i686", database._repository_id.name))
|
||||
assert database.dependencies_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name)) == []
|
||||
assert database.dependencies_get(package_python_schedule.base) == [dependencies2]
|
||||
|
||||
|
||||
def test_dependencies_insert_remove_full(database: SQLite, package_ahriman: Package,
|
||||
package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must remove all dependencies for the repository
|
||||
"""
|
||||
database.dependencies_insert(Dependencies(package_ahriman.base, {Path("usr"): ["python"]}))
|
||||
database.dependencies_insert(Dependencies(package_python_schedule.base, {Path("usr"): ["filesystem"]}))
|
||||
database.dependencies_insert(Dependencies(package_ahriman.base, {Path("usr"): ["python3"]}),
|
||||
RepositoryId("i686", database._repository_id.name))
|
||||
|
||||
database.dependencies_remove(None)
|
||||
assert database.dependencies_get() == []
|
||||
assert database.dependencies_get(package_ahriman.base, RepositoryId("i686", database._repository_id.name))
|
@ -33,3 +33,21 @@ def test_init_skip_migration(database: SQLite, configuration: Configuration, moc
|
||||
|
||||
database.init(configuration)
|
||||
migrate_schema_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_package_clear(database: SQLite, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must clear package data
|
||||
"""
|
||||
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_clear")
|
||||
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
|
||||
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
|
||||
changes_mock = mocker.patch("ahriman.core.database.SQLite.changes_remove")
|
||||
dependencies_mock = mocker.patch("ahriman.core.database.SQLite.dependencies_remove")
|
||||
|
||||
database.package_clear("package")
|
||||
build_queue_mock.assert_called_once_with("package")
|
||||
patches_mock.assert_called_once_with("package", [])
|
||||
logs_mock.assert_called_once_with("package", None)
|
||||
changes_mock.assert_called_once_with("package")
|
||||
dependencies_mock.assert_called_once_with("package")
|
||||
|
@ -82,34 +82,40 @@ def test_make_request(mocker: MockerFixture) -> None:
|
||||
|
||||
auth = client.auth = ("username", "password")
|
||||
assert client.make_request("GET", "url9") is not None
|
||||
client.auth = None
|
||||
|
||||
assert client.make_request("GET", "url10", stream=True) is not None
|
||||
|
||||
request_mock.assert_has_calls([
|
||||
MockCall("GET", "url1", params=None, data=None, headers=None, files=None, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("GET", "url2", params=[("param", "value")], data=None, headers=None, files=None, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("POST", "url3", params=None, data=None, headers=None, files=None, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("POST", "url4", params=None, data=None, headers=None, files=None, json={"param": "value"},
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("POST", "url5", params=None, data={"param": "value"}, headers=None, files=None, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("POST", "url6", params=None, data=None, headers=None, files={"file": "tuple"}, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("DELETE", "url7", params=None, data=None, headers=None, files=None, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("GET", "url8", params=None, data=None, headers={"user-agent": "ua"}, files=None, json=None,
|
||||
auth=None, timeout=client.timeout),
|
||||
stream=None, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("GET", "url9", params=None, data=None, headers=None, files=None, json=None,
|
||||
auth=auth, timeout=client.timeout),
|
||||
stream=None, auth=auth, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
MockCall("GET", "url10", params=None, data=None, headers=None, files=None, json=None,
|
||||
stream=True, auth=None, timeout=client.timeout),
|
||||
MockCall().raise_for_status(),
|
||||
])
|
||||
|
||||
@ -151,4 +157,4 @@ def test_make_request_session() -> None:
|
||||
client.make_request("GET", "url", session=session_mock)
|
||||
session_mock.request.assert_called_once_with(
|
||||
"GET", "url", params=None, data=None, headers=None, files=None, json=None,
|
||||
auth=None, timeout=client.timeout)
|
||||
stream=None, auth=None, timeout=client.timeout)
|
||||
|
@ -85,7 +85,7 @@ def test_remote_update(remote_call: RemoteCall, mocker: MockerFixture) -> None:
|
||||
"aur": False,
|
||||
"local": False,
|
||||
"manual": True,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
def test_remote_wait(remote_call: RemoteCall, mocker: MockerFixture) -> None:
|
||||
|
@ -2,28 +2,37 @@ import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from typing import Any
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.repository.executor import Executor
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.packagers import Packagers
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
def test_process_build(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
def test_process_build(executor: Executor, package_ahriman: Package, passwd: Any, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run build process
|
||||
"""
|
||||
dependencies = Dependencies(package_ahriman.base)
|
||||
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
||||
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
||||
mocker.patch("ahriman.core.build_tools.task.Task.build", return_value=[Path(package_ahriman.base)])
|
||||
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init", return_value="sha")
|
||||
move_mock = mocker.patch("shutil.move")
|
||||
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_building")
|
||||
commit_sha_mock = mocker.patch("ahriman.core.status.client.Client.package_changes_set")
|
||||
depends_on_mock = mocker.patch("ahriman.models.package_archive.PackageArchive.depends_on",
|
||||
return_value=dependencies)
|
||||
dependencies_mock = mocker.patch("ahriman.core.database.SQLite.dependencies_insert")
|
||||
|
||||
executor.process_build([package_ahriman], Packagers("packager"), bump_pkgrel=False)
|
||||
init_mock.assert_called_once_with(pytest.helpers.anyvar(int), pytest.helpers.anyvar(int), None)
|
||||
depends_on_mock.assert_called_once_with()
|
||||
dependencies_mock.assert_called_once_with(dependencies)
|
||||
# must move files (once)
|
||||
move_mock.assert_called_once_with(Path(package_ahriman.base), executor.paths.packages / package_ahriman.base)
|
||||
# must update status
|
||||
@ -70,10 +79,7 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
|
||||
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
||||
tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear")
|
||||
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
|
||||
build_queue_mock = mocker.patch("ahriman.core.database.SQLite.build_queue_clear")
|
||||
patches_mock = mocker.patch("ahriman.core.database.SQLite.patches_remove")
|
||||
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
|
||||
commit_sha_mock = mocker.patch("ahriman.core.database.SQLite.changes_remove")
|
||||
database_mock = mocker.patch("ahriman.core.database.SQLite.package_clear")
|
||||
status_client_mock = mocker.patch("ahriman.core.status.client.Client.package_remove")
|
||||
|
||||
executor.process_remove([package_ahriman.base])
|
||||
@ -82,11 +88,30 @@ def test_process_remove_base(executor: Executor, package_ahriman: Package, mocke
|
||||
package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath)
|
||||
# must update status and remove package files
|
||||
tree_clear_mock.assert_called_once_with(package_ahriman.base)
|
||||
build_queue_mock.assert_called_once_with(package_ahriman.base)
|
||||
patches_mock.assert_called_once_with(package_ahriman.base, [])
|
||||
logs_mock.assert_called_once_with(package_ahriman.base, None)
|
||||
database_mock.assert_called_once_with(package_ahriman.base)
|
||||
status_client_mock.assert_called_once_with(package_ahriman.base)
|
||||
commit_sha_mock.assert_called_once_with(package_ahriman.base)
|
||||
|
||||
|
||||
def test_process_remove_with_debug(executor: Executor, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run remove debug packages too
|
||||
"""
|
||||
package_ahriman.packages = {
|
||||
package_ahriman.base: package_ahriman.packages[package_ahriman.base],
|
||||
f"{package_ahriman.base}-debug": package_ahriman.packages[package_ahriman.base],
|
||||
}
|
||||
mocker.patch("ahriman.core.repository.executor.Executor.packages", return_value=[package_ahriman])
|
||||
mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear")
|
||||
mocker.patch("ahriman.core.database.SQLite.package_clear")
|
||||
mocker.patch("ahriman.core.status.client.Client.package_remove")
|
||||
repo_remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
|
||||
|
||||
executor.process_remove([package_ahriman.base])
|
||||
# must remove via alpm wrapper
|
||||
repo_remove_mock.assert_has_calls([
|
||||
MockCall(package_ahriman.base, package_ahriman.packages[package_ahriman.base].filepath),
|
||||
MockCall(f"{package_ahriman.base}-debug", package_ahriman.packages[package_ahriman.base].filepath),
|
||||
])
|
||||
|
||||
|
||||
def test_process_remove_base_multiple(executor: Executor, package_python_schedule: Package,
|
||||
|
@ -96,17 +96,30 @@ def test_package_changes_skip(package_info: PackageInfo, package_ahriman: Packag
|
||||
changes_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_packages(package_info: PackageInfo, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
def test_packages(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return repository packages
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[package_ahriman])
|
||||
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives")
|
||||
package_info.packages()
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert package_info.packages() == [package_ahriman, package_python_schedule]
|
||||
# it uses filter object, so we cannot verify argument list =/
|
||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_packages_filter(package_info: PackageInfo, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter result by bases
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert package_info.packages([package_ahriman.base]) == [package_ahriman]
|
||||
|
||||
|
||||
def test_packages_built(package_info: PackageInfo, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return build packages
|
||||
|
@ -6,6 +6,7 @@ from typing import Any
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.repository.update_handler import UpdateHandler
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
@ -16,12 +17,14 @@ def test_updates_aur(update_handler: UpdateHandler, package_ahriman: Package,
|
||||
"""
|
||||
must provide updates with status updates
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
|
||||
packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
|
||||
return_value=[package_ahriman])
|
||||
mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
|
||||
status_client_mock = mocker.patch("ahriman.core.status.client.Client.set_pending")
|
||||
package_is_outdated_mock = mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
|
||||
|
||||
assert update_handler.updates_aur([], vcs=True) == [package_ahriman]
|
||||
packages_mock.assert_called_once_with([])
|
||||
status_client_mock.assert_called_once_with(package_ahriman.base)
|
||||
package_is_outdated_mock.assert_called_once_with(
|
||||
package_ahriman, update_handler.paths,
|
||||
@ -70,17 +73,17 @@ def test_updates_aur_local(update_handler: UpdateHandler, package_ahriman: Packa
|
||||
package_load_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
def test_updates_aur_filter(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must provide updates only for filtered packages
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
|
||||
return_value=[package_ahriman])
|
||||
mocker.patch("ahriman.models.package.Package.is_outdated", return_value=True)
|
||||
package_load_mock = mocker.patch("ahriman.models.package.Package.from_aur", return_value=package_ahriman)
|
||||
|
||||
assert update_handler.updates_aur([package_ahriman.base], vcs=True) == [package_ahriman]
|
||||
packages_mock.assert_called_once_with([package_ahriman.base])
|
||||
package_load_mock.assert_called_once_with(package_ahriman.base, None)
|
||||
|
||||
|
||||
@ -131,7 +134,7 @@ def test_updates_aur_load_by_package(update_handler: UpdateHandler, package_pyth
|
||||
assert update_handler.updates_aur([], vcs=True) == [package_python_schedule]
|
||||
|
||||
|
||||
def test_updates_load_by_package_aur_failed(update_handler: UpdateHandler, package_ahriman: Package,
|
||||
def test_updates_aur_load_by_package_failed(update_handler: UpdateHandler, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update status via client for failed load
|
||||
@ -143,6 +146,56 @@ def test_updates_load_by_package_aur_failed(update_handler: UpdateHandler, packa
|
||||
update_handler.updates_aur([], vcs=True)
|
||||
|
||||
|
||||
def test_updates_dependencies(update_handler: UpdateHandler, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must define updates with broken dependencies
|
||||
"""
|
||||
packages_mock = mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
dependencies = [
|
||||
Dependencies(package_ahriman.base, {Path("usr/lib/python3.11/site-packages"): ["python"]}),
|
||||
Dependencies(package_python_schedule.base, {Path("usr/lib/python3.12/site-packages"): ["python"]}),
|
||||
]
|
||||
mocker.patch("ahriman.core.database.SQLite.dependencies_get", return_value=dependencies)
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.files",
|
||||
return_value={"python": {Path("usr/lib/python3.12/site-packages")}})
|
||||
|
||||
assert update_handler.updates_dependencies(["filter"]) == [package_ahriman]
|
||||
packages_mock.assert_called_once_with(["filter"])
|
||||
|
||||
|
||||
def test_updates_dependencies_skip_unknown(update_handler: UpdateHandler, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip unknown package dependencies
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
|
||||
mocker.patch("ahriman.core.database.SQLite.dependencies_get", return_value=[])
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.files",
|
||||
return_value={"python": {Path("usr/lib/python3.12/site-packages")}})
|
||||
|
||||
assert update_handler.updates_dependencies(["filter"]) == []
|
||||
|
||||
|
||||
def test_updates_dependencies_partial(update_handler: UpdateHandler, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip broken dependencies update if at least one package provides file
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.update_handler.UpdateHandler.packages", return_value=[package_ahriman])
|
||||
dependencies = [
|
||||
Dependencies(package_ahriman.base, {Path("usr"): ["filesystem", "python"]}),
|
||||
]
|
||||
mocker.patch("ahriman.core.database.SQLite.dependencies_get", return_value=dependencies)
|
||||
mocker.patch("ahriman.core.alpm.pacman.Pacman.files", return_value={
|
||||
"filesystem": {Path("usr")},
|
||||
"python": {Path("usr")},
|
||||
})
|
||||
|
||||
assert update_handler.updates_dependencies(["filter"]) == []
|
||||
|
||||
|
||||
def test_updates_local(update_handler: UpdateHandler, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must check for updates for locally stored packages
|
||||
|
@ -6,7 +6,6 @@ from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock, call as MockCall
|
||||
|
||||
from ahriman.core.support.pkgbuild.pkgbuild_generator import PkgbuildGenerator
|
||||
from ahriman.core.util import utcnow
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
@ -38,7 +37,7 @@ def test_pkgver(pkgbuild_generator: PkgbuildGenerator, mocker: MockerFixture) ->
|
||||
must implement default version as current date
|
||||
"""
|
||||
mocker.patch("ahriman.core.support.pkgbuild.pkgbuild_generator.utcnow", return_value=datetime.datetime(2002, 3, 11))
|
||||
assert pkgbuild_generator.pkgver == utcnow().strftime("20020311")
|
||||
assert pkgbuild_generator.pkgver == "20020311"
|
||||
|
||||
|
||||
def test_url(pkgbuild_generator: PkgbuildGenerator) -> None:
|
||||
|
@ -480,6 +480,7 @@ def test_walk(resource_path_root: Path) -> None:
|
||||
"""
|
||||
expected = sorted([
|
||||
resource_path_root / "core" / "ahriman.ini",
|
||||
resource_path_root / "core" / "arcanisrepo.files.tar.gz",
|
||||
resource_path_root / "core" / "logging.ini",
|
||||
resource_path_root / "models" / "aur_error",
|
||||
resource_path_root / "models" / "big_file_checksum",
|
||||
@ -487,6 +488,7 @@ def test_walk(resource_path_root: Path) -> None:
|
||||
resource_path_root / "models" / "official_error",
|
||||
resource_path_root / "models" / "package_ahriman_aur",
|
||||
resource_path_root / "models" / "package_akonadi_aur",
|
||||
resource_path_root / "models" / "package_ahriman_files",
|
||||
resource_path_root / "models" / "package_ahriman_srcinfo",
|
||||
resource_path_root / "models" / "package_gcc10_srcinfo",
|
||||
resource_path_root / "models" / "package_jellyfin-ffmpeg5-bin_srcinfo",
|
||||
|
@ -1,7 +1,9 @@
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
@ -10,9 +12,11 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_archive import PackageArchive
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -60,6 +64,25 @@ def internal_status(counters: Counters) -> InternalStatus:
|
||||
repository="aur-clone")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_archive_ahriman(package_ahriman: Package, repository_paths: RepositoryPaths,
|
||||
passwd: Any, mocker: MockerFixture) -> PackageArchive:
|
||||
"""
|
||||
package archive fixture
|
||||
|
||||
Args:
|
||||
package_ahriman(Package): package test instance
|
||||
repository_paths(RepositoryPaths): repository paths test instance
|
||||
passwd(Any): passwd structure test instance
|
||||
mocker(MockerFixture): mocker object
|
||||
|
||||
Returns:
|
||||
PackageArchive: package archive test instance
|
||||
"""
|
||||
mocker.patch("ahriman.models.repository_paths.getpwuid", return_value=passwd)
|
||||
return PackageArchive(repository_paths.build_directory, package_ahriman)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_tpacpi_bat_git() -> Package:
|
||||
"""
|
||||
|
0
tests/ahriman/models/test_dependencies.py
Normal file
0
tests/ahriman/models/test_dependencies.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user