mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-29 05:39:56 +00:00
Compare commits
1 Commits
d1ec695329
...
2.17.2
Author | SHA1 | Date | |
---|---|---|---|
e0f1563f62 |
1067
docs/_static/architecture.dot
vendored
1067
docs/_static/architecture.dot
vendored
File diff suppressed because it is too large
Load Diff
@ -92,14 +92,6 @@ ahriman.core.formatters.repository\_printer module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.core.formatters.repository\_stats\_printer module
|
|
||||||
---------------------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: ahriman.core.formatters.repository_stats_printer
|
|
||||||
:members:
|
|
||||||
:no-undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
ahriman.core.formatters.status\_printer module
|
ahriman.core.formatters.status\_printer module
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -236,14 +236,6 @@ ahriman.models.repository\_paths module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.models.repository\_stats module
|
|
||||||
---------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: ahriman.models.repository_stats
|
|
||||||
:members:
|
|
||||||
:no-undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
ahriman.models.result module
|
ahriman.models.result module
|
||||||
----------------------------
|
----------------------------
|
||||||
|
|
||||||
@ -260,14 +252,6 @@ ahriman.models.scan\_paths module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.models.series\_statistics module
|
|
||||||
----------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: ahriman.models.series_statistics
|
|
||||||
:members:
|
|
||||||
:no-undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
ahriman.models.sign\_settings module
|
ahriman.models.sign\_settings module
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
@ -260,14 +260,6 @@ ahriman.web.schemas.repository\_id\_schema module
|
|||||||
:no-undoc-members:
|
:no-undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
ahriman.web.schemas.repository\_stats\_schema module
|
|
||||||
----------------------------------------------------
|
|
||||||
|
|
||||||
.. automodule:: ahriman.web.schemas.repository_stats_schema
|
|
||||||
:members:
|
|
||||||
:no-undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
ahriman.web.schemas.search\_schema module
|
ahriman.web.schemas.search\_schema module
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
pkgbase='ahriman'
|
pkgbase='ahriman'
|
||||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||||
pkgver=2.17.1
|
pkgver=2.17.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="ArcH linux ReposItory MANager"
|
pkgdesc="ArcH linux ReposItory MANager"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
@ -7,8 +7,6 @@ logging = ahriman.ini.d/logging.ini
|
|||||||
;apply_migrations = yes
|
;apply_migrations = yes
|
||||||
; Path to the application SQLite database.
|
; Path to the application SQLite database.
|
||||||
database = ${repository:root}/ahriman.db
|
database = ${repository:root}/ahriman.db
|
||||||
; Keep last build logs for each package
|
|
||||||
keep_last_logs = 5
|
|
||||||
|
|
||||||
[alpm]
|
[alpm]
|
||||||
; Path to pacman system database cache.
|
; Path to pacman system database cache.
|
||||||
|
@ -16,11 +16,11 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<nav class="navbar navbar-expand-lg">
|
<nav class="navbar navbar-expand-lg">
|
||||||
<div class="navbar-brand"><a href="https://github.com/arcan1s/ahriman" title="logo"><img src="/static/logo.svg" width="30" height="30" alt=""></a></div>
|
<div class="navbar-brand"><a href="https://github.com/arcan1s/ahriman" title="logo"><img src="/static/logo.svg" width="30" height="30" alt=""></a></div>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar" aria-controls="repositories-navbar" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar-supported-content" aria-controls="repositories-navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div id="repositories-navbar" class="collapse navbar-collapse">
|
<div id="repositories-navbar-supported-content" class="collapse navbar-collapse">
|
||||||
<ul id="repositories" class="nav nav-tabs">
|
<ul id="repositories" class="nav nav-tabs">
|
||||||
{% for repository in repositories %}
|
{% for repository in repositories %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
@ -36,9 +36,7 @@
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="toolbar" class="dropdown">
|
<div id="toolbar" class="dropdown">
|
||||||
<button id="dashboard-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#dashboard-modal">
|
<a id="badge-status" tabindex="0" role="button" class="btn btn-outline-secondary" data-bs-toggle="popover" data-bs-trigger="focus" data-bs-content="no run data"><i class="bi bi-info-circle"></i></a>
|
||||||
<i class="bi bi-info-circle"></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{% if not auth.enabled or auth.username is not none %}
|
{% if not auth.enabled or auth.username is not none %}
|
||||||
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
<button type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
@ -154,7 +152,6 @@
|
|||||||
|
|
||||||
{% include "build-status/alerts.jinja2" %}
|
{% include "build-status/alerts.jinja2" %}
|
||||||
|
|
||||||
{% include "build-status/dashboard.jinja2" %}
|
|
||||||
{% include "build-status/package-add-modal.jinja2" %}
|
{% include "build-status/package-add-modal.jinja2" %}
|
||||||
{% include "build-status/package-rebuild-modal.jinja2" %}
|
{% include "build-status/package-rebuild-modal.jinja2" %}
|
||||||
{% include "build-status/key-import-modal.jinja2" %}
|
{% include "build-status/key-import-modal.jinja2" %}
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
<div id="dashboard-modal" tabindex="-1" role="dialog" class="modal fade">
|
|
||||||
<div class="modal-dialog modal-xl" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div id="dashboard-modal-header" class="modal-header">
|
|
||||||
<h4 class="modal-title">System health</h4>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="form-group row mt-2">
|
|
||||||
<div class="col-4 col-lg-2" style="text-align: right">Repository name</div>
|
|
||||||
<div id="dashboard-name" class="col-8 col-lg-3"></div>
|
|
||||||
<div class="col-4 col-lg-2" style="text-align: right">Repository architecture</div>
|
|
||||||
<div id="dashboard-architecture" class="col-8 col-lg-3"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group row mt-2">
|
|
||||||
<div class="col-4 col-lg-2" style="text-align: right">Current status</div>
|
|
||||||
<div id="dashboard-status" class="col-8 col-lg-3"></div>
|
|
||||||
<div class="col-4 col-lg-2" style="text-align: right">Updated at</div>
|
|
||||||
<div id="dashboard-status-timestamp" class="col-8 col-lg-3"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dashboard-canvas" class="form-group row mt-2">
|
|
||||||
<div class="col-8 col-lg-6">
|
|
||||||
<canvas id="dashboard-packages-count-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
<div class="col-8 col-lg-6">
|
|
||||||
<canvas id="dashboard-packages-statuses-chart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const dashboardModal = document.getElementById("dashboard-modal");
|
|
||||||
const dashboardModalHeader = document.getElementById("dashboard-modal-header");
|
|
||||||
|
|
||||||
const dashboardName = document.getElementById("dashboard-name");
|
|
||||||
const dashboardArchitecture = document.getElementById("dashboard-architecture");
|
|
||||||
const dashboardStatus = document.getElementById("dashboard-status");
|
|
||||||
const dashboardStatusTimestamp = document.getElementById("dashboard-status-timestamp");
|
|
||||||
|
|
||||||
const dashboardCanvas = document.getElementById("dashboard-canvas");
|
|
||||||
const dashboardPackagesStatusesChartCanvas = document.getElementById("dashboard-packages-statuses-chart");
|
|
||||||
let dashboardPackagesStatusesChart = null;
|
|
||||||
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
|
|
||||||
let dashboardPackagesCountChart = null;
|
|
||||||
|
|
||||||
ready(_ => {
|
|
||||||
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
|
|
||||||
type: "pie",
|
|
||||||
data: {},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
dashboardPackagesCountChart = new Chart(dashboardPackagesCountChartCanvas, {
|
|
||||||
type: "bar",
|
|
||||||
data: {},
|
|
||||||
options: {
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
responsive: true,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -59,14 +59,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<div class="tab-content" id="nav-tabContent">
|
<div class="tab-content" id="nav-tabContent">
|
||||||
<div id="package-info-logs" class="tab-pane fade show active" role="tabpanel" aria-labelledby="package-info-logs-button" tabindex="0">
|
<div id="package-info-logs" class="tab-pane fade show active" role="tabpanel" aria-labelledby="package-info-logs-button" tabindex="0">
|
||||||
<div class="row">
|
<pre class="language-console"><code id="package-info-logs-input" class="pre-scrollable language-console"></code><button id="package-info-logs-copy-button" type="button" class="btn language-console" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
<div class="col-2">
|
|
||||||
<nav id="package-info-logs-versions" class="nav flex-column"></nav>
|
|
||||||
</div>
|
|
||||||
<div class="col-10">
|
|
||||||
<pre class="language-console"><code id="package-info-logs-input" class="pre-scrollable language-console"></code><button id="package-info-logs-copy-button" type="button" class="btn language-console" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="package-info-changes" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-changes-button" tabindex="0">
|
<div id="package-info-changes" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-changes-button" tabindex="0">
|
||||||
<pre class="language-diff"><code id="package-info-changes-input" class="pre-scrollable language-diff"></code><button id="package-info-changes-copy-button" type="button" class="btn language-diff" onclick="copyChanges()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
<pre class="language-diff"><code id="package-info-changes-input" class="pre-scrollable language-diff"></code><button id="package-info-changes-copy-button" type="button" class="btn language-diff" onclick="copyChanges()"><i class="bi bi-clipboard"></i> copy</button></pre>
|
||||||
@ -107,7 +100,6 @@
|
|||||||
const packageInfoModalHeader = document.getElementById("package-info-modal-header");
|
const packageInfoModalHeader = document.getElementById("package-info-modal-header");
|
||||||
const packageInfo = document.getElementById("package-info");
|
const packageInfo = document.getElementById("package-info");
|
||||||
|
|
||||||
const packageInfoLogsVersions = document.getElementById("package-info-logs-versions");
|
|
||||||
const packageInfoLogsInput = document.getElementById("package-info-logs-input");
|
const packageInfoLogsInput = document.getElementById("package-info-logs-input");
|
||||||
const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button");
|
const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button");
|
||||||
|
|
||||||
@ -293,51 +285,25 @@
|
|||||||
convert: response => response.json(),
|
convert: response => response.json(),
|
||||||
},
|
},
|
||||||
data => {
|
data => {
|
||||||
const selectors = Object
|
const logs = data.map(log_record => {
|
||||||
.values(
|
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
|
||||||
data.reduce((acc, log_record) => {
|
});
|
||||||
const id = `${log_record.version}-${log_record.process_id}`;
|
packageInfoLogsInput.textContent = logs.join("\n");
|
||||||
if (acc[id])
|
highlight(packageInfoLogsInput);
|
||||||
acc[id].created = Math.min(log_record.created, acc[id].created);
|
|
||||||
else
|
|
||||||
acc[id] = log_record;
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
)
|
|
||||||
.sort(({created: left}, {created: right}) =>
|
|
||||||
right - left
|
|
||||||
)
|
|
||||||
.map(version => {
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.classList.add("nav-link");
|
|
||||||
|
|
||||||
link.textContent = version.version;
|
|
||||||
link.href = "#";
|
|
||||||
link.onclick = _ => {
|
|
||||||
const logs = data
|
|
||||||
.filter(log_record => log_record.version === version.version && log_record.process_id === version.process_id)
|
|
||||||
.map(log_record => `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`);
|
|
||||||
|
|
||||||
packageInfoLogsInput.textContent = logs.join("\n");
|
|
||||||
highlight(packageInfoLogsInput);
|
|
||||||
|
|
||||||
Array.from(packageInfoLogsVersions.children).forEach(el => el.classList.remove("active"));
|
|
||||||
link.classList.add("active");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return link;
|
|
||||||
});
|
|
||||||
|
|
||||||
packageInfoLogsVersions.replaceChildren(...selectors);
|
|
||||||
selectors.find(Boolean)?.click();
|
|
||||||
},
|
},
|
||||||
onFailure,
|
onFailure,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPackage(packageBase, onFailure) {
|
function loadPackage(packageBase, onFailure) {
|
||||||
|
const headerClass = status => {
|
||||||
|
if (status === "pending") return ["bg-warning"];
|
||||||
|
if (status === "building") return ["bg-warning"];
|
||||||
|
if (status === "failed") return ["bg-danger", "text-white"];
|
||||||
|
if (status === "success") return ["bg-success", "text-white"];
|
||||||
|
return ["bg-secondary", "text-white"];
|
||||||
|
};
|
||||||
|
|
||||||
makeRequest(
|
makeRequest(
|
||||||
`/api/v1/packages/${packageBase}`,
|
`/api/v1/packages/${packageBase}`,
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
// so far bootstrap-table only operates with jquery elements
|
// so far bootstrap-table only operates with jquery elements
|
||||||
const table = $(document.getElementById("packages"));
|
const table = $(document.getElementById("packages"));
|
||||||
|
|
||||||
const dashboardButton = document.getElementById("dashboard-button");
|
const statusBadge = document.getElementById("badge-status");
|
||||||
const versionBadge = document.getElementById("badge-version");
|
const versionBadge = document.getElementById("badge-version");
|
||||||
|
|
||||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||||
@ -141,62 +141,14 @@
|
|||||||
data => {
|
data => {
|
||||||
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
||||||
|
|
||||||
dashboardButton.classList.remove(...dashboardButton.classList);
|
statusBadge.classList.remove(...statusBadge.classList);
|
||||||
dashboardButton.classList.add("btn");
|
statusBadge.classList.add("btn");
|
||||||
dashboardButton.classList.add(badgeClass(data.status.status));
|
statusBadge.classList.add(badgeClass(data.status.status));
|
||||||
|
|
||||||
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
|
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||||
dashboardModalHeader.classList.add("modal-header");
|
popover.dispose();
|
||||||
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
|
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`;
|
||||||
|
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||||
dashboardName.textContent = data.repository;
|
|
||||||
dashboardArchitecture.textContent = data.architecture;
|
|
||||||
dashboardStatus.textContent = data.status.status;
|
|
||||||
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
|
|
||||||
|
|
||||||
if (dashboardPackagesStatusesChart) {
|
|
||||||
const labels = [
|
|
||||||
"unknown",
|
|
||||||
"pending",
|
|
||||||
"building",
|
|
||||||
"failed",
|
|
||||||
"success",
|
|
||||||
];
|
|
||||||
dashboardPackagesStatusesChart.config.data = {
|
|
||||||
labels: labels,
|
|
||||||
datasets: [{
|
|
||||||
label: "packages in status",
|
|
||||||
data: labels.map(label => data.packages[label]),
|
|
||||||
backgroundColor: [
|
|
||||||
"rgb(55, 58, 60)",
|
|
||||||
"rgb(255, 117, 24)",
|
|
||||||
"rgb(255, 117, 24)",
|
|
||||||
"rgb(255, 0, 57)",
|
|
||||||
"rgb(63, 182, 24)", // copy-paste from current style
|
|
||||||
],
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
dashboardPackagesStatusesChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dashboardPackagesCountChart) {
|
|
||||||
dashboardPackagesCountChart.config.data = {
|
|
||||||
labels: ["packages"],
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "archives",
|
|
||||||
data: [data.stats.packages],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "bases",
|
|
||||||
data: [data.stats.bases],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
dashboardPackagesCountChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
dashboardCanvas.hidden = data.status.total > 0;
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -275,6 +227,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||||
selectRepository();
|
selectRepository();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -58,14 +58,6 @@
|
|||||||
return value.includes(dataList[index].toLowerCase());
|
return value.includes(dataList[index].toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
function headerClass(status) {
|
|
||||||
if (status === "pending") return ["bg-warning"];
|
|
||||||
if (status === "building") return ["bg-warning"];
|
|
||||||
if (status === "failed") return ["bg-danger", "text-white"];
|
|
||||||
if (status === "success") return ["bg-success", "text-white"];
|
|
||||||
return ["bg-secondary", "text-white"];
|
|
||||||
}
|
|
||||||
|
|
||||||
function listToTable(data) {
|
function listToTable(data) {
|
||||||
return Array.from(new Set(data))
|
return Array.from(new Set(data))
|
||||||
.sort()
|
.sort()
|
||||||
|
@ -27,10 +27,4 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
pointer-events: none;
|
|
||||||
cursor: default;
|
|
||||||
color: black !important;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual"
|
.TH AHRIMAN "1" "2025\-02\-23" "ahriman" "Generated Python Manual"
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ahriman
|
ahriman
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
|
@ -17,4 +17,4 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2.17.1"
|
__version__ = "2.17.2"
|
||||||
|
@ -27,7 +27,7 @@ from pathlib import Path
|
|||||||
from ahriman.application.application import Application
|
from ahriman.application.application import Application
|
||||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||||
from ahriman.core.configuration import Configuration
|
from ahriman.core.configuration import Configuration
|
||||||
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter, RepositoryStatsPrinter
|
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter
|
||||||
from ahriman.core.utils import enum_values, pretty_datetime
|
from ahriman.core.utils import enum_values, pretty_datetime
|
||||||
from ahriman.models.event import Event, EventType
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
@ -64,7 +64,6 @@ class Statistics(Handler):
|
|||||||
|
|
||||||
match args.package:
|
match args.package:
|
||||||
case None:
|
case None:
|
||||||
RepositoryStatsPrinter(repository_id, application.reporter.statistics())(verbose=True)
|
|
||||||
Statistics.stats_per_package(args.event, events, args.chart)
|
Statistics.stats_per_package(args.event, events, args.chart)
|
||||||
case _:
|
case _:
|
||||||
Statistics.stats_for_package(args.event, events, args.chart)
|
Statistics.stats_for_package(args.event, events, args.chart)
|
||||||
|
@ -45,11 +45,6 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
|||||||
"path_exists": True,
|
"path_exists": True,
|
||||||
"path_type": "dir",
|
"path_type": "dir",
|
||||||
},
|
},
|
||||||
"keep_last_logs": {
|
|
||||||
"type": "integer",
|
|
||||||
"coerce": "integer",
|
|
||||||
"min": 0,
|
|
||||||
},
|
|
||||||
"logging": {
|
"logging": {
|
||||||
"type": "path",
|
"type": "path",
|
||||||
"coerce": "absolute_path",
|
"coerce": "absolute_path",
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2025 ahriman team.
|
|
||||||
#
|
|
||||||
# This file is part of ahriman
|
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
__all__ = ["steps"]
|
|
||||||
|
|
||||||
|
|
||||||
steps = [
|
|
||||||
"""
|
|
||||||
alter table logs add column process_id text not null default ''
|
|
||||||
""",
|
|
||||||
]
|
|
@ -30,7 +30,7 @@ class LogsOperations(Operations):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
|
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
|
||||||
repository_id: RepositoryId | None = None) -> list[tuple[LogRecordId, float, str]]:
|
repository_id: RepositoryId | None = None) -> list[tuple[float, str]]:
|
||||||
"""
|
"""
|
||||||
extract logs for specified package base
|
extract logs for specified package base
|
||||||
|
|
||||||
@ -41,16 +41,16 @@ class LogsOperations(Operations):
|
|||||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
list[tuple[LogRecordId, float, str]]: sorted package log records and their timestamps
|
list[tuple[float, str]]: sorted package log records and their timestamps
|
||||||
"""
|
"""
|
||||||
repository_id = repository_id or self._repository_id
|
repository_id = repository_id or self._repository_id
|
||||||
|
|
||||||
def run(connection: Connection) -> list[tuple[LogRecordId, float, str]]:
|
def run(connection: Connection) -> list[tuple[float, str]]:
|
||||||
return [
|
return [
|
||||||
(LogRecordId(package_base, row["version"], row["process_id"]), row["created"], row["record"])
|
(row["created"], row["record"])
|
||||||
for row in connection.execute(
|
for row in connection.execute(
|
||||||
"""
|
"""
|
||||||
select created, record, version, process_id from (
|
select created, record from (
|
||||||
select * from logs
|
select * from logs
|
||||||
where package_base = :package_base and repository = :repository
|
where package_base = :package_base and repository = :repository
|
||||||
order by created desc limit :limit offset :offset
|
order by created desc limit :limit offset :offset
|
||||||
@ -83,9 +83,9 @@ class LogsOperations(Operations):
|
|||||||
connection.execute(
|
connection.execute(
|
||||||
"""
|
"""
|
||||||
insert into logs
|
insert into logs
|
||||||
(package_base, version, created, record, repository, process_id)
|
(package_base, version, created, record, repository)
|
||||||
values
|
values
|
||||||
(:package_base, :version, :created, :record, :repository, :process_id)
|
(:package_base, :version, :created, :record, :repository)
|
||||||
""",
|
""",
|
||||||
{
|
{
|
||||||
"package_base": log_record_id.package_base,
|
"package_base": log_record_id.package_base,
|
||||||
@ -93,7 +93,6 @@ class LogsOperations(Operations):
|
|||||||
"created": created,
|
"created": created,
|
||||||
"record": record,
|
"record": record,
|
||||||
"repository": repository_id.id,
|
"repository": repository_id.id,
|
||||||
"process_id": log_record_id.process_id,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -126,54 +125,3 @@ class LogsOperations(Operations):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return self.with_connection(run, commit=True)
|
return self.with_connection(run, commit=True)
|
||||||
|
|
||||||
def logs_rotate(self, keep_last_records: int, repository_id: RepositoryId | None = None) -> None:
|
|
||||||
"""
|
|
||||||
rotate logs in storage. This method will remove old logs, keeping only the last N records for each package
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keep_last_records(int): number of last records to keep
|
|
||||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
|
||||||
"""
|
|
||||||
repository_id = repository_id or self._repository_id
|
|
||||||
|
|
||||||
def remove_duplicates(connection: Connection) -> None:
|
|
||||||
connection.execute(
|
|
||||||
"""
|
|
||||||
delete from logs
|
|
||||||
where (package_base, version, repository, process_id) not in (
|
|
||||||
select package_base, version, repository, process_id from logs
|
|
||||||
where (package_base, version, repository, created) in (
|
|
||||||
select package_base, version, repository, max(created) from logs
|
|
||||||
where repository = :repository
|
|
||||||
group by package_base, version, repository
|
|
||||||
)
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"repository": repository_id.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def remove_older(connection: Connection) -> None:
|
|
||||||
connection.execute(
|
|
||||||
"""
|
|
||||||
delete from logs
|
|
||||||
where (package_base, repository, process_id) in (
|
|
||||||
select package_base, repository, process_id from logs
|
|
||||||
where repository = :repository
|
|
||||||
group by package_base, repository, process_id
|
|
||||||
order by min(created) desc limit -1 offset :offset
|
|
||||||
)
|
|
||||||
""",
|
|
||||||
{
|
|
||||||
"offset": keep_last_records,
|
|
||||||
"repository": repository_id.id,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(connection: Connection) -> None:
|
|
||||||
remove_duplicates(connection)
|
|
||||||
remove_older(connection)
|
|
||||||
|
|
||||||
return self.with_connection(run, commit=True)
|
|
||||||
|
@ -28,7 +28,6 @@ from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
|||||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||||
from ahriman.core.formatters.printer import Printer
|
from ahriman.core.formatters.printer import Printer
|
||||||
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
from ahriman.core.formatters.repository_printer import RepositoryPrinter
|
||||||
from ahriman.core.formatters.repository_stats_printer import RepositoryStatsPrinter
|
|
||||||
from ahriman.core.formatters.status_printer import StatusPrinter
|
from ahriman.core.formatters.status_printer import StatusPrinter
|
||||||
from ahriman.core.formatters.string_printer import StringPrinter
|
from ahriman.core.formatters.string_printer import StringPrinter
|
||||||
from ahriman.core.formatters.tree_printer import TreePrinter
|
from ahriman.core.formatters.tree_printer import TreePrinter
|
||||||
|
@ -17,9 +17,11 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import statistics
|
||||||
|
|
||||||
from ahriman.core.formatters.string_printer import StringPrinter
|
from ahriman.core.formatters.string_printer import StringPrinter
|
||||||
|
from ahriman.core.utils import minmax
|
||||||
from ahriman.models.property import Property
|
from ahriman.models.property import Property
|
||||||
from ahriman.models.series_statistics import SeriesStatistics
|
|
||||||
|
|
||||||
|
|
||||||
class EventStatsPrinter(StringPrinter):
|
class EventStatsPrinter(StringPrinter):
|
||||||
@ -27,7 +29,7 @@ class EventStatsPrinter(StringPrinter):
|
|||||||
print event statistics
|
print event statistics
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
statistics(SeriesStatistics): statistics object
|
events(list[float | int]): event values to build statistics
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, event_type: str, events: list[float | int]) -> None:
|
def __init__(self, event_type: str, events: list[float | int]) -> None:
|
||||||
@ -37,7 +39,7 @@ class EventStatsPrinter(StringPrinter):
|
|||||||
events(list[float | int]): event values to build statistics
|
events(list[float | int]): event values to build statistics
|
||||||
"""
|
"""
|
||||||
StringPrinter.__init__(self, event_type)
|
StringPrinter.__init__(self, event_type)
|
||||||
self.statistics = SeriesStatistics(events)
|
self.events = events
|
||||||
|
|
||||||
def properties(self) -> list[Property]:
|
def properties(self) -> list[Property]:
|
||||||
"""
|
"""
|
||||||
@ -47,17 +49,24 @@ class EventStatsPrinter(StringPrinter):
|
|||||||
list[Property]: list of content properties
|
list[Property]: list of content properties
|
||||||
"""
|
"""
|
||||||
properties = [
|
properties = [
|
||||||
Property("total", self.statistics.total),
|
Property("total", len(self.events)),
|
||||||
]
|
]
|
||||||
|
|
||||||
# time statistics
|
# time statistics
|
||||||
if self.statistics:
|
if self.events:
|
||||||
mean = self.statistics.mean
|
min_time, max_time = minmax(self.events)
|
||||||
|
mean = statistics.mean(self.events)
|
||||||
|
|
||||||
|
if len(self.events) > 1:
|
||||||
|
st_dev = statistics.stdev(self.events)
|
||||||
|
average = f"{mean:.3f} ± {st_dev:.3f}"
|
||||||
|
else:
|
||||||
|
average = f"{mean:.3f}"
|
||||||
|
|
||||||
properties.extend([
|
properties.extend([
|
||||||
Property("min", self.statistics.min),
|
Property("min", min_time),
|
||||||
Property("average", f"{mean:.3f} ± {self.statistics.st_dev:.3f}"),
|
Property("average", average),
|
||||||
Property("max", self.statistics.max),
|
Property("max", max_time),
|
||||||
])
|
])
|
||||||
|
|
||||||
return properties
|
return properties
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2025 ahriman team.
|
|
||||||
#
|
|
||||||
# This file is part of ahriman
|
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from ahriman.core.formatters.string_printer import StringPrinter
|
|
||||||
from ahriman.core.utils import pretty_size
|
|
||||||
from ahriman.models.property import Property
|
|
||||||
from ahriman.models.repository_id import RepositoryId
|
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryStatsPrinter(StringPrinter):
|
|
||||||
"""
|
|
||||||
print repository statistics
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
statistics(RepositoryStats): repository statistics
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, repository_id: RepositoryId, statistics: RepositoryStats) -> None:
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
statistics(RepositoryStats): repository statistics
|
|
||||||
"""
|
|
||||||
StringPrinter.__init__(self, str(repository_id))
|
|
||||||
self.statistics = statistics
|
|
||||||
|
|
||||||
def properties(self) -> list[Property]:
|
|
||||||
"""
|
|
||||||
convert content into printable data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[Property]: list of content properties
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
Property("Packages", self.statistics.bases),
|
|
||||||
Property("Repository size", pretty_size(self.statistics.archive_size)),
|
|
||||||
]
|
|
@ -17,7 +17,6 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import atexit
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Self
|
from typing import Self
|
||||||
@ -34,7 +33,6 @@ class HttpLogHandler(logging.Handler):
|
|||||||
method
|
method
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
keep_last_records(int): number of last records to keep
|
|
||||||
reporter(Client): build status reporter instance
|
reporter(Client): build status reporter instance
|
||||||
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
|
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
|
||||||
"""
|
"""
|
||||||
@ -53,7 +51,6 @@ class HttpLogHandler(logging.Handler):
|
|||||||
|
|
||||||
self.reporter = Client.load(repository_id, configuration, report=report)
|
self.reporter = Client.load(repository_id, configuration, report=report)
|
||||||
self.suppress_errors = suppress_errors
|
self.suppress_errors = suppress_errors
|
||||||
self.keep_last_records = configuration.getint("settings", "keep_last_logs", fallback=0)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
|
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
|
||||||
@ -79,8 +76,6 @@ class HttpLogHandler(logging.Handler):
|
|||||||
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
|
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
|
||||||
root.addHandler(handler)
|
root.addHandler(handler)
|
||||||
|
|
||||||
atexit.register(handler.rotate)
|
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
def emit(self, record: logging.LogRecord) -> None:
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
@ -100,9 +95,3 @@ class HttpLogHandler(logging.Handler):
|
|||||||
if self.suppress_errors:
|
if self.suppress_errors:
|
||||||
return
|
return
|
||||||
self.handleError(record)
|
self.handleError(record)
|
||||||
|
|
||||||
def rotate(self) -> None:
|
|
||||||
"""
|
|
||||||
rotate log records, removing older ones
|
|
||||||
"""
|
|
||||||
self.reporter.logs_rotate(self.keep_last_records)
|
|
||||||
|
@ -74,7 +74,7 @@ class LazyLogging:
|
|||||||
|
|
||||||
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
|
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
|
||||||
record = current_factory(*args, **kwargs)
|
record = current_factory(*args, **kwargs)
|
||||||
record.package_id = LogRecordId(package_base, version or "<unknown>")
|
record.package_id = LogRecordId(package_base, version or "")
|
||||||
return record
|
return record
|
||||||
|
|
||||||
logging.setLogRecordFactory(package_record_factory)
|
logging.setLogRecordFactory(package_record_factory)
|
||||||
@ -99,3 +99,24 @@ class LazyLogging:
|
|||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
self._package_logger_reset()
|
self._package_logger_reset()
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def suppress_logging(self, log_level: int = logging.WARNING) -> Generator[None, None, None]:
|
||||||
|
"""
|
||||||
|
silence log messages in context
|
||||||
|
|
||||||
|
Args:
|
||||||
|
log_level(int, optional): the highest log level to keep (Default value = logging.WARNING)
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
This function is designed to be used to suppress all log messages in context, e.g.:
|
||||||
|
|
||||||
|
>>> with self.suppress_logging():
|
||||||
|
>>> do_some_noisy_actions()
|
||||||
|
"""
|
||||||
|
current_level = self.logger.manager.disable
|
||||||
|
try:
|
||||||
|
logging.disable(log_level)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
logging.disable(current_level)
|
||||||
|
@ -144,7 +144,8 @@ class UpdateHandler(PackageInfo, Cleaner):
|
|||||||
branch="master",
|
branch="master",
|
||||||
)
|
)
|
||||||
|
|
||||||
Sources.fetch(cache_dir, source)
|
with self.suppress_logging():
|
||||||
|
Sources.fetch(cache_dir, source)
|
||||||
remote = Package.from_build(cache_dir, self.architecture, None)
|
remote = Package.from_build(cache_dir, self.architecture, None)
|
||||||
|
|
||||||
local = packages.get(remote.base)
|
local = packages.get(remote.base)
|
||||||
|
@ -31,7 +31,6 @@ from ahriman.models.log_record_id import LogRecordId
|
|||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
class Client:
|
||||||
@ -115,14 +114,6 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def logs_rotate(self, keep_last_records: int) -> None:
|
|
||||||
"""
|
|
||||||
remove older logs from storage
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keep_last_records(int): number of last records to keep
|
|
||||||
"""
|
|
||||||
|
|
||||||
def package_changes_get(self, package_base: str) -> Changes:
|
def package_changes_get(self, package_base: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
get package changes
|
get package changes
|
||||||
@ -205,8 +196,7 @@ class Client:
|
|||||||
"""
|
"""
|
||||||
# this method does not raise NotImplementedError because it is actively used as dummy client for http log
|
# this method does not raise NotImplementedError because it is actively used as dummy client for http log
|
||||||
|
|
||||||
def package_logs_get(self, package_base: str, limit: int = -1,
|
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||||
offset: int = 0) -> list[tuple[LogRecordId, float, str]]:
|
|
||||||
"""
|
"""
|
||||||
get package logs
|
get package logs
|
||||||
|
|
||||||
@ -216,7 +206,7 @@ class Client:
|
|||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[tuple[LogRecordId, float, str]]: package logs
|
list[tuple[float, str]]: package logs
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: not implemented method
|
NotImplementedError: not implemented method
|
||||||
@ -364,16 +354,6 @@ class Client:
|
|||||||
return # skip update in case if package is already known
|
return # skip update in case if package is already known
|
||||||
self.package_update(package, BuildStatusEnum.Unknown)
|
self.package_update(package, BuildStatusEnum.Unknown)
|
||||||
|
|
||||||
def statistics(self) -> RepositoryStats:
|
|
||||||
"""
|
|
||||||
get repository statistics
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RepositoryStats: repository statistics object
|
|
||||||
"""
|
|
||||||
packages = [package for package, _ in self.package_get(None)]
|
|
||||||
return RepositoryStats.from_packages(packages)
|
|
||||||
|
|
||||||
def status_get(self) -> InternalStatus:
|
def status_get(self) -> InternalStatus:
|
||||||
"""
|
"""
|
||||||
get internal service status
|
get internal service status
|
||||||
|
@ -75,15 +75,6 @@ class LocalClient(Client):
|
|||||||
"""
|
"""
|
||||||
return self.database.event_get(event, object_id, from_date, to_date, limit, offset, self.repository_id)
|
return self.database.event_get(event, object_id, from_date, to_date, limit, offset, self.repository_id)
|
||||||
|
|
||||||
def logs_rotate(self, keep_last_records: int) -> None:
|
|
||||||
"""
|
|
||||||
remove older logs from storage
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keep_last_records(int): number of last records to keep
|
|
||||||
"""
|
|
||||||
self.database.logs_rotate(keep_last_records, self.repository_id)
|
|
||||||
|
|
||||||
def package_changes_get(self, package_base: str) -> Changes:
|
def package_changes_get(self, package_base: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
get package changes
|
get package changes
|
||||||
@ -154,8 +145,7 @@ class LocalClient(Client):
|
|||||||
"""
|
"""
|
||||||
self.database.logs_insert(log_record_id, created, message, self.repository_id)
|
self.database.logs_insert(log_record_id, created, message, self.repository_id)
|
||||||
|
|
||||||
def package_logs_get(self, package_base: str, limit: int = -1,
|
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||||
offset: int = 0) -> list[tuple[LogRecordId, float, str]]:
|
|
||||||
"""
|
"""
|
||||||
get package logs
|
get package logs
|
||||||
|
|
||||||
@ -165,7 +155,7 @@ class LocalClient(Client):
|
|||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[tuple[LogRecordId, float, str]]: package logs
|
list[tuple[float, str]]: package logs
|
||||||
"""
|
"""
|
||||||
return self.database.logs_get(package_base, limit, offset, self.repository_id)
|
return self.database.logs_get(package_base, limit, offset, self.repository_id)
|
||||||
|
|
||||||
|
@ -53,6 +53,9 @@ class Watcher(LazyLogging):
|
|||||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||||
self.status = BuildStatus()
|
self.status = BuildStatus()
|
||||||
|
|
||||||
|
# special variables for updating logs
|
||||||
|
self._last_log_record_id = LogRecordId("", "")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def packages(self) -> list[tuple[Package, BuildStatus]]:
|
def packages(self) -> list[tuple[Package, BuildStatus]]:
|
||||||
"""
|
"""
|
||||||
@ -78,8 +81,6 @@ class Watcher(LazyLogging):
|
|||||||
for package, status in self.client.package_get(None)
|
for package, status in self.client.package_get(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
logs_rotate: Callable[[int], None]
|
|
||||||
|
|
||||||
package_changes_get: Callable[[str], Changes]
|
package_changes_get: Callable[[str], Changes]
|
||||||
|
|
||||||
package_changes_update: Callable[[str, Changes], None]
|
package_changes_update: Callable[[str, Changes], None]
|
||||||
@ -107,9 +108,22 @@ class Watcher(LazyLogging):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise UnknownPackageError(package_base) from None
|
raise UnknownPackageError(package_base) from None
|
||||||
|
|
||||||
package_logs_add: Callable[[LogRecordId, float, str], None]
|
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||||
|
"""
|
||||||
|
make new log record into database
|
||||||
|
|
||||||
package_logs_get: Callable[[str, int, int], list[tuple[LogRecordId, float, str]]]
|
Args:
|
||||||
|
log_record_id(LogRecordId): log record id
|
||||||
|
created(float): log created timestamp
|
||||||
|
message(str): log message
|
||||||
|
"""
|
||||||
|
if self._last_log_record_id != log_record_id:
|
||||||
|
# there is new log record, so we remove old ones
|
||||||
|
self.package_logs_remove(log_record_id.package_base, log_record_id.version)
|
||||||
|
self._last_log_record_id = log_record_id
|
||||||
|
self.client.package_logs_add(log_record_id, created, message)
|
||||||
|
|
||||||
|
package_logs_get: Callable[[str, int, int], list[tuple[float, str]]]
|
||||||
|
|
||||||
package_logs_remove: Callable[[str, str | None], None]
|
package_logs_remove: Callable[[str, str | None], None]
|
||||||
|
|
||||||
|
@ -210,18 +210,6 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def logs_rotate(self, keep_last_records: int) -> None:
|
|
||||||
"""
|
|
||||||
remove older logs from storage
|
|
||||||
|
|
||||||
Args:
|
|
||||||
keep_last_records(int): number of last records to keep
|
|
||||||
"""
|
|
||||||
query = self.repository_id.query() + [("keep_last_records", str(keep_last_records))]
|
|
||||||
|
|
||||||
with contextlib.suppress(Exception):
|
|
||||||
self.make_request("DELETE", f"{self.address}/api/v1/service/logs", params=query)
|
|
||||||
|
|
||||||
def package_changes_get(self, package_base: str) -> Changes:
|
def package_changes_get(self, package_base: str) -> Changes:
|
||||||
"""
|
"""
|
||||||
get package changes
|
get package changes
|
||||||
@ -318,7 +306,6 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
payload = {
|
payload = {
|
||||||
"created": created,
|
"created": created,
|
||||||
"message": message,
|
"message": message,
|
||||||
"process_id": log_record_id.process_id,
|
|
||||||
"version": log_record_id.version,
|
"version": log_record_id.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,8 +315,7 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
self.make_request("POST", self._logs_url(log_record_id.package_base),
|
self.make_request("POST", self._logs_url(log_record_id.package_base),
|
||||||
params=self.repository_id.query(), json=payload, suppress_errors=True)
|
params=self.repository_id.query(), json=payload, suppress_errors=True)
|
||||||
|
|
||||||
def package_logs_get(self, package_base: str, limit: int = -1,
|
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||||
offset: int = 0) -> list[tuple[LogRecordId, float, str]]:
|
|
||||||
"""
|
"""
|
||||||
get package logs
|
get package logs
|
||||||
|
|
||||||
@ -339,7 +325,7 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
offset(int, optional): records offset (Default value = 0)
|
offset(int, optional): records offset (Default value = 0)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list[tuple[LogRecordId, float, str]]: package logs
|
list[tuple[float, str]]: package logs
|
||||||
"""
|
"""
|
||||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||||
|
|
||||||
@ -347,13 +333,7 @@ class WebClient(Client, SyncAhrimanClient):
|
|||||||
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
|
|
||||||
return [
|
return [(record["created"], record["message"]) for record in response_json]
|
||||||
(
|
|
||||||
LogRecordId(package_base, record["version"], record["process_id"]),
|
|
||||||
record["created"],
|
|
||||||
record["message"]
|
|
||||||
) for record in response_json
|
|
||||||
]
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -23,7 +23,6 @@ from typing import Any, Self
|
|||||||
from ahriman.core.utils import dataclass_view
|
from ahriman.core.utils import dataclass_view
|
||||||
from ahriman.models.build_status import BuildStatus
|
from ahriman.models.build_status import BuildStatus
|
||||||
from ahriman.models.counters import Counters
|
from ahriman.models.counters import Counters
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
@ -36,7 +35,6 @@ class InternalStatus:
|
|||||||
architecture(str | None): repository architecture
|
architecture(str | None): repository architecture
|
||||||
packages(Counters): packages statuses counter object
|
packages(Counters): packages statuses counter object
|
||||||
repository(str | None): repository name
|
repository(str | None): repository name
|
||||||
stats(RepositoryStats | None): repository stats
|
|
||||||
version(str | None): service version
|
version(str | None): service version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -44,7 +42,6 @@ class InternalStatus:
|
|||||||
architecture: str | None = None
|
architecture: str | None = None
|
||||||
packages: Counters = field(default=Counters(total=0))
|
packages: Counters = field(default=Counters(total=0))
|
||||||
repository: str | None = None
|
repository: str | None = None
|
||||||
stats: RepositoryStats | None = None
|
|
||||||
version: str | None = None
|
version: str | None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -59,13 +56,11 @@ class InternalStatus:
|
|||||||
Self: internal status
|
Self: internal status
|
||||||
"""
|
"""
|
||||||
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
counters = Counters.from_json(dump["packages"]) if "packages" in dump else Counters(total=0)
|
||||||
stats = RepositoryStats.from_json(dump["stats"]) if "stats" in dump else None
|
|
||||||
build_status = dump.get("status") or {}
|
build_status = dump.get("status") or {}
|
||||||
return cls(status=BuildStatus.from_json(build_status),
|
return cls(status=BuildStatus.from_json(build_status),
|
||||||
architecture=dump.get("architecture"),
|
architecture=dump.get("architecture"),
|
||||||
packages=counters,
|
packages=counters,
|
||||||
repository=dump.get("repository"),
|
repository=dump.get("repository"),
|
||||||
stats=stats,
|
|
||||||
version=dump.get("version"))
|
version=dump.get("version"))
|
||||||
|
|
||||||
def view(self) -> dict[str, Any]:
|
def view(self) -> dict[str, Any]:
|
||||||
|
@ -17,9 +17,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import uuid
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@ -30,12 +28,7 @@ class LogRecordId:
|
|||||||
Attributes:
|
Attributes:
|
||||||
package_base(str): package base for which log record belongs
|
package_base(str): package base for which log record belongs
|
||||||
version(str): package version for which log record belongs
|
version(str): package version for which log record belongs
|
||||||
process_id(str, optional): unique process identifier
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
package_base: str
|
package_base: str
|
||||||
version: str
|
version: str
|
||||||
|
|
||||||
# this is not mistake, this value is kind of global identifier, which is generated
|
|
||||||
# upon the process start
|
|
||||||
process_id: str = field(default=str(uuid.uuid4()))
|
|
||||||
|
@ -429,11 +429,12 @@ class Package(LazyLogging):
|
|||||||
task = Task(self, configuration, repository_id.architecture, paths)
|
task = Task(self, configuration, repository_id.architecture, paths)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
with self.suppress_logging():
|
||||||
task.init(paths.cache_for(self.base), [], None)
|
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
||||||
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
|
task.init(paths.cache_for(self.base), [], None)
|
||||||
|
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
|
||||||
|
|
||||||
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
||||||
except Exception:
|
except Exception:
|
||||||
self.logger.exception("cannot determine version of VCS package")
|
self.logger.exception("cannot determine version of VCS package")
|
||||||
finally:
|
finally:
|
||||||
|
@ -97,12 +97,3 @@ class RepositoryId:
|
|||||||
raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'")
|
raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'")
|
||||||
|
|
||||||
return (self.name, self.architecture) < (other.name, other.architecture)
|
return (self.name, self.architecture) < (other.name, other.architecture)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""
|
|
||||||
string representation of the repository identifier
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: string view of the repository identifier
|
|
||||||
"""
|
|
||||||
return f"{self.name} ({self.architecture})"
|
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2025 ahriman team.
|
|
||||||
#
|
|
||||||
# This file is part of ahriman
|
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from dataclasses import dataclass, fields
|
|
||||||
from typing import Any, Self
|
|
||||||
|
|
||||||
from ahriman.core.utils import filter_json
|
|
||||||
from ahriman.models.package import Package
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class RepositoryStats:
|
|
||||||
"""
|
|
||||||
repository stats representation
|
|
||||||
"""
|
|
||||||
|
|
||||||
bases: int
|
|
||||||
packages: int
|
|
||||||
archive_size: int
|
|
||||||
installed_size: int
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
|
||||||
"""
|
|
||||||
construct counters from json dump
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dump(dict[str, Any]): json dump body
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Self: status counters
|
|
||||||
"""
|
|
||||||
# filter to only known fields
|
|
||||||
known_fields = [pair.name for pair in fields(cls)]
|
|
||||||
return cls(**filter_json(dump, known_fields))
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_packages(cls, packages: list[Package]) -> Self:
|
|
||||||
"""
|
|
||||||
construct statistics from list of repository packages
|
|
||||||
|
|
||||||
Args:
|
|
||||||
packages(list[Packages]): list of repository packages
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Self: constructed statistics object
|
|
||||||
"""
|
|
||||||
return cls(
|
|
||||||
bases=len(packages),
|
|
||||||
packages=sum(len(package.packages) for package in packages),
|
|
||||||
archive_size=sum(
|
|
||||||
archive.archive_size or 0
|
|
||||||
for package in packages
|
|
||||||
for archive in package.packages.values()
|
|
||||||
),
|
|
||||||
installed_size=sum(
|
|
||||||
archive.installed_size or 0
|
|
||||||
for package in packages
|
|
||||||
for archive in package.packages.values()
|
|
||||||
),
|
|
||||||
)
|
|
@ -1,104 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2025 ahriman team.
|
|
||||||
#
|
|
||||||
# This file is part of ahriman
|
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
import statistics
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SeriesStatistics:
|
|
||||||
"""
|
|
||||||
series statistics helper
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
series(list[float | int]): list of values to be processed
|
|
||||||
"""
|
|
||||||
|
|
||||||
series: list[float | int]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max(self) -> float | int | None:
|
|
||||||
"""
|
|
||||||
get max value in series
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float | int | None: ``None`` if series is empty and maximal value otherwise``
|
|
||||||
"""
|
|
||||||
if self:
|
|
||||||
return max(self.series)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def mean(self) -> float | int | None:
|
|
||||||
"""
|
|
||||||
get mean value in series
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float | int | None: ``None`` if series is empty and mean value otherwise
|
|
||||||
"""
|
|
||||||
if self:
|
|
||||||
return statistics.mean(self.series)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def min(self) -> float | int | None:
|
|
||||||
"""
|
|
||||||
get min value in series
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float | int | None: ``None`` if series is empty and minimal value otherwise
|
|
||||||
"""
|
|
||||||
if self:
|
|
||||||
return min(self.series)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def st_dev(self) -> float | None:
|
|
||||||
"""
|
|
||||||
get standard deviation in series
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
float | None: ``None`` if series size is less than 1, 0 if series contains single element and standard
|
|
||||||
deviation otherwise
|
|
||||||
"""
|
|
||||||
if not self:
|
|
||||||
return None
|
|
||||||
if len(self.series) > 1:
|
|
||||||
return statistics.stdev(self.series)
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def total(self) -> int:
|
|
||||||
"""
|
|
||||||
retrieve amount of elements
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: the series collection size
|
|
||||||
"""
|
|
||||||
return len(self.series)
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
|
||||||
"""
|
|
||||||
check if series is empty or not
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: ``True`` if series contains elements and ``False`` otherwise
|
|
||||||
"""
|
|
||||||
return bool(self.total)
|
|
@ -18,8 +18,7 @@
|
|||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from aiohttp.web import HTTPException
|
from aiohttp.web import HTTPException
|
||||||
from collections.abc import Callable
|
from typing import Any, Callable
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec import Schema, aiohttp_apispec
|
from ahriman.web.apispec import Schema, aiohttp_apispec
|
||||||
|
@ -31,7 +31,6 @@ from ahriman.web.schemas.info_schema import InfoSchema
|
|||||||
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||||
from ahriman.web.schemas.log_schema import LogSchema
|
from ahriman.web.schemas.log_schema import LogSchema
|
||||||
from ahriman.web.schemas.login_schema import LoginSchema
|
from ahriman.web.schemas.login_schema import LoginSchema
|
||||||
from ahriman.web.schemas.logs_rotate_schema import LogsRotateSchema
|
|
||||||
from ahriman.web.schemas.logs_schema import LogsSchema
|
from ahriman.web.schemas.logs_schema import LogsSchema
|
||||||
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
||||||
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||||
@ -50,8 +49,8 @@ from ahriman.web.schemas.process_id_schema import ProcessIdSchema
|
|||||||
from ahriman.web.schemas.process_schema import ProcessSchema
|
from ahriman.web.schemas.process_schema import ProcessSchema
|
||||||
from ahriman.web.schemas.remote_schema import RemoteSchema
|
from ahriman.web.schemas.remote_schema import RemoteSchema
|
||||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||||
from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema
|
|
||||||
from ahriman.web.schemas.search_schema import SearchSchema
|
from ahriman.web.schemas.search_schema import SearchSchema
|
||||||
from ahriman.web.schemas.status_schema import StatusSchema
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema
|
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema
|
||||||
|
from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema
|
||||||
from ahriman.web.schemas.worker_schema import WorkerSchema
|
from ahriman.web.schemas.worker_schema import WorkerSchema
|
||||||
|
@ -25,6 +25,6 @@ class FileSchema(Schema):
|
|||||||
request file upload schema
|
request file upload schema
|
||||||
"""
|
"""
|
||||||
|
|
||||||
archive = fields.Raw(required=True, metadata={
|
archive = fields.Field(required=True, metadata={
|
||||||
"description": "Package archive to be uploaded",
|
"description": "Package archive to be uploaded",
|
||||||
})
|
})
|
||||||
|
@ -21,7 +21,6 @@ from ahriman import __version__
|
|||||||
from ahriman.web.apispec import fields
|
from ahriman.web.apispec import fields
|
||||||
from ahriman.web.schemas.counters_schema import CountersSchema
|
from ahriman.web.schemas.counters_schema import CountersSchema
|
||||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||||
from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema
|
|
||||||
from ahriman.web.schemas.status_schema import StatusSchema
|
from ahriman.web.schemas.status_schema import StatusSchema
|
||||||
|
|
||||||
|
|
||||||
@ -33,9 +32,6 @@ class InternalStatusSchema(RepositoryIdSchema):
|
|||||||
packages = fields.Nested(CountersSchema(), required=True, metadata={
|
packages = fields.Nested(CountersSchema(), required=True, metadata={
|
||||||
"description": "Repository package counters",
|
"description": "Repository package counters",
|
||||||
})
|
})
|
||||||
stats = fields.Nested(RepositoryStatsSchema(), required=True, metadata={
|
|
||||||
"description": "Repository stats",
|
|
||||||
})
|
|
||||||
status = fields.Nested(StatusSchema(), required=True, metadata={
|
status = fields.Nested(StatusSchema(), required=True, metadata={
|
||||||
"description": "Repository status as stored by web service",
|
"description": "Repository status as stored by web service",
|
||||||
})
|
})
|
||||||
|
@ -17,13 +17,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from ahriman import __version__
|
|
||||||
from ahriman.web.apispec import Schema, fields
|
from ahriman.web.apispec import Schema, fields
|
||||||
|
|
||||||
|
|
||||||
class LogSchema(Schema):
|
class LogSchema(Schema):
|
||||||
"""
|
"""
|
||||||
request and response package log schema
|
request package log schema
|
||||||
"""
|
"""
|
||||||
|
|
||||||
created = fields.Float(required=True, metadata={
|
created = fields.Float(required=True, metadata={
|
||||||
@ -33,10 +32,3 @@ class LogSchema(Schema):
|
|||||||
message = fields.String(required=True, metadata={
|
message = fields.String(required=True, metadata={
|
||||||
"description": "Log message",
|
"description": "Log message",
|
||||||
})
|
})
|
||||||
version = fields.String(required=True, metadata={
|
|
||||||
"description": "Package version to tag",
|
|
||||||
"example": __version__,
|
|
||||||
})
|
|
||||||
process_id = fields.String(metadata={
|
|
||||||
"description": "Process unique identifier",
|
|
||||||
})
|
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2025 ahriman team.
|
|
||||||
#
|
|
||||||
# This file is part of ahriman
|
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from ahriman.web.apispec import Schema, fields
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryStatsSchema(Schema):
|
|
||||||
"""
|
|
||||||
response repository stats schema
|
|
||||||
"""
|
|
||||||
|
|
||||||
bases = fields.Int(metadata={
|
|
||||||
"description": "Amount of unique packages bases",
|
|
||||||
"example": 2,
|
|
||||||
})
|
|
||||||
packages = fields.Int(metadata={
|
|
||||||
"description": "Amount of unique packages",
|
|
||||||
"example": 4,
|
|
||||||
})
|
|
||||||
archive_size = fields.Int(metadata={
|
|
||||||
"description": "Total archive size of the packages in bytes",
|
|
||||||
"example": 42000,
|
|
||||||
})
|
|
||||||
installed_size = fields.Int(metadata={
|
|
||||||
"description": "Total installed size of the packages in bytes",
|
|
||||||
"example": 42000000,
|
|
||||||
})
|
|
@ -17,14 +17,18 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from ahriman.web.apispec import Schema, fields
|
from ahriman import __version__
|
||||||
|
from ahriman.web.apispec import fields
|
||||||
|
from ahriman.web.schemas.log_schema import LogSchema
|
||||||
|
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||||
|
|
||||||
|
|
||||||
class LogsRotateSchema(Schema):
|
class VersionedLogSchema(LogSchema, RepositoryIdSchema):
|
||||||
"""
|
"""
|
||||||
request logs rotate schema
|
request package log schema
|
||||||
"""
|
"""
|
||||||
|
|
||||||
keep_last_records = fields.Integer(metadata={
|
version = fields.Integer(required=True, metadata={
|
||||||
"description": "Keep the specified amount of records",
|
"description": "Package version to tag",
|
||||||
|
"example": __version__,
|
||||||
})
|
})
|
@ -24,7 +24,8 @@ from ahriman.core.utils import pretty_datetime
|
|||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
from ahriman.web.schemas import LogSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema
|
from ahriman.web.schemas import LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema, \
|
||||||
|
VersionedLogSchema
|
||||||
from ahriman.web.views.base import BaseView
|
from ahriman.web.views.base import BaseView
|
||||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
response = {
|
response = {
|
||||||
"package_base": package_base,
|
"package_base": package_base,
|
||||||
"status": status.view(),
|
"status": status.view(),
|
||||||
"logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for _, created, message in logs)
|
"logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for created, message in logs)
|
||||||
}
|
}
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
|
||||||
@ -108,7 +109,7 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
error_400_enabled=True,
|
error_400_enabled=True,
|
||||||
error_404_description="Repository is unknown",
|
error_404_description="Repository is unknown",
|
||||||
match_schema=PackageNameSchema,
|
match_schema=PackageNameSchema,
|
||||||
body_schema=LogSchema,
|
body_schema=VersionedLogSchema,
|
||||||
)
|
)
|
||||||
async def post(self) -> None:
|
async def post(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -128,8 +129,6 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
raise HTTPBadRequest(reason=str(ex))
|
||||||
|
|
||||||
# either read from process identifier from payload or assign to the current process identifier
|
self.service().package_logs_add(LogRecordId(package_base, version), created, record)
|
||||||
process_id = data.get("process_id", LogRecordId("", "").process_id)
|
|
||||||
self.service().package_logs_add(LogRecordId(package_base, version, process_id), created, record)
|
|
||||||
|
|
||||||
raise HTTPNoContent
|
raise HTTPNoContent
|
||||||
|
@ -1,63 +0,0 @@
|
|||||||
#
|
|
||||||
# Copyright (c) 2021-2025 ahriman team.
|
|
||||||
#
|
|
||||||
# This file is part of ahriman
|
|
||||||
# (see https://github.com/arcan1s/ahriman).
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent
|
|
||||||
|
|
||||||
from ahriman.models.user_access import UserAccess
|
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
|
||||||
from ahriman.web.schemas import LogsRotateSchema
|
|
||||||
from ahriman.web.views.base import BaseView
|
|
||||||
|
|
||||||
|
|
||||||
class LogsView(BaseView):
|
|
||||||
"""
|
|
||||||
logs management web view
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
|
|
||||||
"""
|
|
||||||
|
|
||||||
DELETE_PERMISSION = UserAccess.Full
|
|
||||||
ROUTES = ["/api/v1/service/logs"]
|
|
||||||
|
|
||||||
@apidocs(
|
|
||||||
tags=["Actions"],
|
|
||||||
summary="Rotate logs",
|
|
||||||
description="Remove older logs from system",
|
|
||||||
permission=DELETE_PERMISSION,
|
|
||||||
error_400_enabled=True,
|
|
||||||
error_404_description="Repository is unknown",
|
|
||||||
query_schema=LogsRotateSchema,
|
|
||||||
)
|
|
||||||
async def delete(self) -> None:
|
|
||||||
"""
|
|
||||||
rotate logs from system
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPBadRequest: if bad data is supplied
|
|
||||||
HTTPNoContent: on success response
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
keep_last_records = int(self.request.query.get("keep_last_records", 0))
|
|
||||||
except Exception as ex:
|
|
||||||
raise HTTPBadRequest(reason=str(ex))
|
|
||||||
|
|
||||||
self.service().logs_rotate(keep_last_records)
|
|
||||||
|
|
||||||
raise HTTPNoContent
|
|
@ -23,7 +23,6 @@ from ahriman import __version__
|
|||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
from ahriman.models.counters import Counters
|
from ahriman.models.counters import Counters
|
||||||
from ahriman.models.internal_status import InternalStatus
|
from ahriman.models.internal_status import InternalStatus
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.apispec.decorators import apidocs
|
from ahriman.web.apispec.decorators import apidocs
|
||||||
from ahriman.web.schemas import InternalStatusSchema, RepositoryIdSchema, StatusSchema
|
from ahriman.web.schemas import InternalStatusSchema, RepositoryIdSchema, StatusSchema
|
||||||
@ -61,16 +60,12 @@ class StatusView(StatusViewGuard, BaseView):
|
|||||||
Response: 200 with service status object
|
Response: 200 with service status object
|
||||||
"""
|
"""
|
||||||
repository_id = self.repository_id()
|
repository_id = self.repository_id()
|
||||||
packages = self.service(repository_id).packages
|
counters = Counters.from_packages(self.service(repository_id).packages)
|
||||||
counters = Counters.from_packages(packages)
|
|
||||||
stats = RepositoryStats.from_packages([package for package, _ in packages])
|
|
||||||
|
|
||||||
status = InternalStatus(
|
status = InternalStatus(
|
||||||
status=self.service(repository_id).status,
|
status=self.service(repository_id).status,
|
||||||
architecture=repository_id.architecture,
|
architecture=repository_id.architecture,
|
||||||
packages=counters,
|
packages=counters,
|
||||||
repository=repository_id.name,
|
repository=repository_id.name,
|
||||||
stats=stats,
|
|
||||||
version=__version__,
|
version=__version__,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -67,8 +67,6 @@ class LogsView(StatusViewGuard, BaseView):
|
|||||||
{
|
{
|
||||||
"created": created,
|
"created": created,
|
||||||
"message": message,
|
"message": message,
|
||||||
"version": log_record_id.version,
|
} for created, message in logs
|
||||||
"process_id": log_record_id.process_id,
|
|
||||||
} for log_record_id, created, message in logs
|
|
||||||
]
|
]
|
||||||
return json_response(response)
|
return json_response(response)
|
||||||
|
@ -11,7 +11,6 @@ from ahriman.core.repository import Repository
|
|||||||
from ahriman.core.utils import pretty_datetime, utcnow
|
from ahriman.core.utils import pretty_datetime, utcnow
|
||||||
from ahriman.models.event import Event, EventType
|
from ahriman.models.event import Event, EventType
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||||
@ -41,16 +40,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
|
|||||||
"""
|
"""
|
||||||
args = _default_args(args)
|
args = _default_args(args)
|
||||||
events = [Event("1", "1"), Event("2", "2")]
|
events = [Event("1", "1"), Event("2", "2")]
|
||||||
stats = RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4)
|
|
||||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||||
events_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=events)
|
events_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=events)
|
||||||
stats_mock = mocker.patch("ahriman.core.status.client.Client.statistics", return_value=stats)
|
|
||||||
application_mock = mocker.patch("ahriman.application.handlers.statistics.Statistics.stats_per_package")
|
application_mock = mocker.patch("ahriman.application.handlers.statistics.Statistics.stats_per_package")
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
Statistics.run(args, repository_id, configuration, report=False)
|
Statistics.run(args, repository_id, configuration, report=False)
|
||||||
events_mock.assert_called_once_with(args.event, args.package, None, None, args.limit, args.offset)
|
events_mock.assert_called_once_with(args.event, args.package, None, None, args.limit, args.offset)
|
||||||
stats_mock.assert_called_once_with()
|
|
||||||
application_mock.assert_called_once_with(args.event, events, args.chart)
|
application_mock.assert_called_once_with(args.event, events, args.chart)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
from ahriman.core.database.migrations.m015_logs_process_id import steps
|
|
||||||
|
|
||||||
|
|
||||||
def test_migration_logs_process_id() -> None:
|
|
||||||
"""
|
|
||||||
migration must not be empty
|
|
||||||
"""
|
|
||||||
assert steps
|
|
@ -14,12 +14,8 @@ def test_logs_insert_remove_version(database: SQLite, package_ahriman: Package,
|
|||||||
database.logs_insert(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3")
|
database.logs_insert(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3")
|
||||||
|
|
||||||
database.logs_remove(package_ahriman.base, "1")
|
database.logs_remove(package_ahriman.base, "1")
|
||||||
assert database.logs_get(package_ahriman.base) == [
|
assert database.logs_get(package_ahriman.base) == [(42.0, "message 1")]
|
||||||
(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
assert database.logs_get(package_python_schedule.base) == [(42.0, "message 3")]
|
||||||
]
|
|
||||||
assert database.logs_get(package_python_schedule.base) == [
|
|
||||||
(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_insert_remove_multi(database: SQLite, package_ahriman: Package) -> None:
|
def test_logs_insert_remove_multi(database: SQLite, package_ahriman: Package) -> None:
|
||||||
@ -32,7 +28,7 @@ def test_logs_insert_remove_multi(database: SQLite, package_ahriman: Package) ->
|
|||||||
|
|
||||||
database.logs_remove(package_ahriman.base, None, RepositoryId("i686", database._repository_id.name))
|
database.logs_remove(package_ahriman.base, None, RepositoryId("i686", database._repository_id.name))
|
||||||
assert not database.logs_get(package_ahriman.base, repository_id=RepositoryId("i686", database._repository_id.name))
|
assert not database.logs_get(package_ahriman.base, repository_id=RepositoryId("i686", database._repository_id.name))
|
||||||
assert database.logs_get(package_ahriman.base) == [(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")]
|
assert database.logs_get(package_ahriman.base) == [(42.0, "message 1")]
|
||||||
|
|
||||||
|
|
||||||
def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
|
def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||||
@ -45,9 +41,7 @@ def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, pac
|
|||||||
|
|
||||||
database.logs_remove(package_ahriman.base, None)
|
database.logs_remove(package_ahriman.base, None)
|
||||||
assert not database.logs_get(package_ahriman.base)
|
assert not database.logs_get(package_ahriman.base)
|
||||||
assert database.logs_get(package_python_schedule.base) == [
|
assert database.logs_get(package_python_schedule.base) == [(42.0, "message 3")]
|
||||||
(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
||||||
@ -56,10 +50,7 @@ def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
|||||||
"""
|
"""
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2")
|
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2")
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
|
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
|
||||||
assert database.logs_get(package_ahriman.base) == [
|
assert database.logs_get(package_ahriman.base) == [(42.0, "message 1"), (43.0, "message 2")]
|
||||||
(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
|
||||||
(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package) -> None:
|
def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package) -> None:
|
||||||
@ -68,9 +59,7 @@ def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package)
|
|||||||
"""
|
"""
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
|
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2")
|
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2")
|
||||||
assert database.logs_get(package_ahriman.base, 1, 1) == [
|
assert database.logs_get(package_ahriman.base, 1, 1) == [(42.0, "message 1")]
|
||||||
(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> None:
|
def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> None:
|
||||||
@ -82,40 +71,5 @@ def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> No
|
|||||||
RepositoryId("i686", database._repository_id.name))
|
RepositoryId("i686", database._repository_id.name))
|
||||||
|
|
||||||
assert database.logs_get(package_ahriman.base,
|
assert database.logs_get(package_ahriman.base,
|
||||||
repository_id=RepositoryId("i686", database._repository_id.name)) == [
|
repository_id=RepositoryId("i686", database._repository_id.name)) == [(43.0, "message 2")]
|
||||||
(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"),
|
assert database.logs_get(package_ahriman.base) == [(42.0, "message 1")]
|
||||||
]
|
|
||||||
assert database.logs_get(package_ahriman.base) == [
|
|
||||||
(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate_remove_all(database: SQLite, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must remove all records when rotating with keep_last_records is 0
|
|
||||||
"""
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1")
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2")
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "2"), 44.0, "message 3")
|
|
||||||
|
|
||||||
database.logs_rotate(0)
|
|
||||||
assert not database.logs_get(package_ahriman.base)
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate_remove_duplicates(database: SQLite, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must remove duplicate records while preserving the most recent one for each package version
|
|
||||||
"""
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1")
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1", "p2"), 43.0, "message 2")
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "1", "p3"), 44.0, "message 3")
|
|
||||||
database.logs_insert(LogRecordId(package_ahriman.base, "2", "p1"), 45.0, "message 4")
|
|
||||||
|
|
||||||
database.logs_rotate(2)
|
|
||||||
|
|
||||||
logs = database.logs_get(package_ahriman.base)
|
|
||||||
assert len(logs) == 2
|
|
||||||
assert logs == [
|
|
||||||
(LogRecordId(package_ahriman.base, "1", "p3"), 44.0, "message 3"),
|
|
||||||
(LogRecordId(package_ahriman.base, "2", "p1"), 45.0, "message 4"),
|
|
||||||
]
|
|
||||||
|
@ -12,7 +12,6 @@ from ahriman.core.formatters import \
|
|||||||
PackageStatsPrinter, \
|
PackageStatsPrinter, \
|
||||||
PatchPrinter, \
|
PatchPrinter, \
|
||||||
RepositoryPrinter, \
|
RepositoryPrinter, \
|
||||||
RepositoryStatsPrinter, \
|
|
||||||
StatusPrinter, \
|
StatusPrinter, \
|
||||||
StringPrinter, \
|
StringPrinter, \
|
||||||
TreePrinter, \
|
TreePrinter, \
|
||||||
@ -26,7 +25,6 @@ from ahriman.models.changes import Changes
|
|||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_id import RepositoryId
|
from ahriman.models.repository_id import RepositoryId
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
from ahriman.models.user import User
|
from ahriman.models.user import User
|
||||||
|
|
||||||
|
|
||||||
@ -136,29 +134,12 @@ def repository_printer(repository_id: RepositoryId) -> RepositoryPrinter:
|
|||||||
"""
|
"""
|
||||||
fixture for repository printer
|
fixture for repository printer
|
||||||
|
|
||||||
Args:
|
|
||||||
repository_id(RepositoryId): repository identifier fixture
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
RepositoryPrinter: repository printer test instance
|
RepositoryPrinter: repository printer test instance
|
||||||
"""
|
"""
|
||||||
return RepositoryPrinter(repository_id)
|
return RepositoryPrinter(repository_id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def repository_stats_printer(repository_id: RepositoryId) -> RepositoryStatsPrinter:
|
|
||||||
"""
|
|
||||||
fixture for repository stats printer
|
|
||||||
|
|
||||||
Args:
|
|
||||||
repository_id(RepositoryId): repository identifier fixture
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RepositoryStatsPrinter: repository stats printer test instance
|
|
||||||
"""
|
|
||||||
return RepositoryStatsPrinter(repository_id, RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def status_printer() -> StatusPrinter:
|
def status_printer() -> StatusPrinter:
|
||||||
"""
|
"""
|
||||||
|
@ -15,6 +15,13 @@ def test_properties_empty() -> None:
|
|||||||
assert EventStatsPrinter("event", []).properties()
|
assert EventStatsPrinter("event", []).properties()
|
||||||
|
|
||||||
|
|
||||||
|
def test_properties_single() -> None:
|
||||||
|
"""
|
||||||
|
must skip calculation of the standard deviation for single event
|
||||||
|
"""
|
||||||
|
assert EventStatsPrinter("event", [1]).properties()
|
||||||
|
|
||||||
|
|
||||||
def test_title(event_stats_printer: EventStatsPrinter) -> None:
|
def test_title(event_stats_printer: EventStatsPrinter) -> None:
|
||||||
"""
|
"""
|
||||||
must return non-empty title
|
must return non-empty title
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
from ahriman.core.formatters import RepositoryStatsPrinter
|
|
||||||
|
|
||||||
|
|
||||||
def test_properties(repository_stats_printer: RepositoryStatsPrinter) -> None:
|
|
||||||
"""
|
|
||||||
must return non-empty properties list
|
|
||||||
"""
|
|
||||||
assert repository_stats_printer.properties()
|
|
||||||
|
|
||||||
|
|
||||||
def test_title(repository_stats_printer: RepositoryStatsPrinter) -> None:
|
|
||||||
"""
|
|
||||||
must return non-empty title
|
|
||||||
"""
|
|
||||||
assert repository_stats_printer.title()
|
|
@ -19,14 +19,12 @@ def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
|
|||||||
|
|
||||||
add_mock = mocker.patch("logging.Logger.addHandler")
|
add_mock = mocker.patch("logging.Logger.addHandler")
|
||||||
load_mock = mocker.patch("ahriman.core.status.Client.load")
|
load_mock = mocker.patch("ahriman.core.status.Client.load")
|
||||||
atexit_mock = mocker.patch("atexit.register")
|
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
_, repository_id = configuration.check_loaded()
|
||||||
handler = HttpLogHandler.load(repository_id, configuration, report=False)
|
handler = HttpLogHandler.load(repository_id, configuration, report=False)
|
||||||
assert handler
|
assert handler
|
||||||
add_mock.assert_called_once_with(handler)
|
add_mock.assert_called_once_with(handler)
|
||||||
load_mock.assert_called_once_with(repository_id, configuration, report=False)
|
load_mock.assert_called_once_with(repository_id, configuration, report=False)
|
||||||
atexit_mock.assert_called_once_with(handler.rotate)
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_exist(configuration: Configuration) -> None:
|
def test_load_exist(configuration: Configuration) -> None:
|
||||||
@ -95,16 +93,3 @@ def test_emit_skip(configuration: Configuration, log_record: logging.LogRecord,
|
|||||||
|
|
||||||
handler.emit(log_record)
|
handler.emit(log_record)
|
||||||
log_mock.assert_not_called()
|
log_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_rotate(configuration: Configuration, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must rotate logs
|
|
||||||
"""
|
|
||||||
rotate_mock = mocker.patch("ahriman.core.status.Client.logs_rotate")
|
|
||||||
|
|
||||||
_, repository_id = configuration.check_loaded()
|
|
||||||
handler = HttpLogHandler(repository_id, configuration, report=False, suppress_errors=False)
|
|
||||||
|
|
||||||
handler.rotate()
|
|
||||||
rotate_mock.assert_called_once_with(handler.keep_last_records)
|
|
||||||
|
@ -87,3 +87,13 @@ def test_in_package_context_failed(database: SQLite, package_ahriman: Package, m
|
|||||||
raise ValueError()
|
raise ValueError()
|
||||||
|
|
||||||
reset_mock.assert_called_once_with()
|
reset_mock.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
def test_suppress_logging(database: SQLite, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must temporary disable log messages
|
||||||
|
"""
|
||||||
|
disable_mock = mocker.patch("ahriman.core.log.lazy_logging.logging.disable")
|
||||||
|
with database.suppress_logging():
|
||||||
|
pass
|
||||||
|
disable_mock.assert_has_calls([MockCall(logging.WARNING), MockCall(logging.NOTSET)])
|
||||||
|
@ -16,7 +16,6 @@ from ahriman.models.internal_status import InternalStatus
|
|||||||
from ahriman.models.log_record_id import LogRecordId
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
def test_load_dummy_client(configuration: Configuration) -> None:
|
def test_load_dummy_client(configuration: Configuration) -> None:
|
||||||
@ -112,13 +111,6 @@ def test_event_get(client: Client) -> None:
|
|||||||
client.event_get(None, None)
|
client.event_get(None, None)
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate(client: Client, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must do not raise exception on logs rotation call
|
|
||||||
"""
|
|
||||||
client.logs_rotate(1)
|
|
||||||
|
|
||||||
|
|
||||||
def test_package_changes_get(client: Client, package_ahriman: Package) -> None:
|
def test_package_changes_get(client: Client, package_ahriman: Package) -> None:
|
||||||
"""
|
"""
|
||||||
must raise not implemented on package changes request
|
must raise not implemented on package changes request
|
||||||
@ -293,14 +285,6 @@ def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: Mock
|
|||||||
update_mock.assert_not_called()
|
update_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_statistics(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must correctly fetch statistics
|
|
||||||
"""
|
|
||||||
mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)])
|
|
||||||
assert client.statistics() == RepositoryStats(bases=1, packages=1, archive_size=4200, installed_size=4200000)
|
|
||||||
|
|
||||||
|
|
||||||
def test_status_get(client: Client) -> None:
|
def test_status_get(client: Client) -> None:
|
||||||
"""
|
"""
|
||||||
must return dummy status for web service
|
must return dummy status for web service
|
||||||
|
@ -34,15 +34,6 @@ def test_event_get(local_client: LocalClient, package_ahriman: Package, mocker:
|
|||||||
local_client.repository_id)
|
local_client.repository_id)
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must rotate logs
|
|
||||||
"""
|
|
||||||
rotate_mock = mocker.patch("ahriman.core.database.SQLite.logs_rotate")
|
|
||||||
local_client.logs_rotate(42)
|
|
||||||
rotate_mock.assert_called_once_with(42, local_client.repository_id)
|
|
||||||
|
|
||||||
|
|
||||||
def test_package_changes_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_changes_get(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must retrieve package changes
|
must retrieve package changes
|
||||||
|
@ -5,6 +5,7 @@ from pytest_mock import MockerFixture
|
|||||||
from ahriman.core.exceptions import UnknownPackageError
|
from ahriman.core.exceptions import UnknownPackageError
|
||||||
from ahriman.core.status.watcher import Watcher
|
from ahriman.core.status.watcher import Watcher
|
||||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||||
|
from ahriman.models.log_record_id import LogRecordId
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
|
|
||||||
|
|
||||||
@ -63,6 +64,38 @@ def test_package_get_failed(watcher: Watcher, package_ahriman: Package) -> None:
|
|||||||
watcher.package_get(package_ahriman.base)
|
watcher.package_get(package_ahriman.base)
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_logs_add_new(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create package logs record for new package
|
||||||
|
"""
|
||||||
|
delete_mock = mocker.patch("ahriman.core.status.watcher.Watcher.package_logs_remove", create=True)
|
||||||
|
insert_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_add")
|
||||||
|
|
||||||
|
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.version)
|
||||||
|
assert watcher._last_log_record_id != log_record_id
|
||||||
|
|
||||||
|
watcher.package_logs_add(log_record_id, 42.01, "log record")
|
||||||
|
delete_mock.assert_called_once_with(package_ahriman.base, log_record_id.version)
|
||||||
|
insert_mock.assert_called_once_with(log_record_id, 42.01, "log record")
|
||||||
|
|
||||||
|
assert watcher._last_log_record_id == log_record_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_logs_add_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
"""
|
||||||
|
must create package logs record for current package
|
||||||
|
"""
|
||||||
|
delete_mock = mocker.patch("ahriman.core.status.watcher.Watcher.package_logs_remove", create=True)
|
||||||
|
insert_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_add")
|
||||||
|
|
||||||
|
log_record_id = LogRecordId(package_ahriman.base, watcher._last_log_record_id.version)
|
||||||
|
watcher._last_log_record_id = log_record_id
|
||||||
|
|
||||||
|
watcher.package_logs_add(log_record_id, 42.01, "log record")
|
||||||
|
delete_mock.assert_not_called()
|
||||||
|
insert_mock.assert_called_once_with(log_record_id, 42.01, "log record")
|
||||||
|
|
||||||
|
|
||||||
def test_package_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must remove package base
|
must remove package base
|
||||||
|
@ -257,57 +257,6 @@ def test_event_get_failed_http_error_suppress(web_client: WebClient, mocker: Moc
|
|||||||
logging_mock.assert_not_called()
|
logging_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must rotate logs
|
|
||||||
"""
|
|
||||||
requests_mock = mocker.patch("ahriman.core.status.web_client.WebClient.make_request")
|
|
||||||
|
|
||||||
web_client.logs_rotate(42)
|
|
||||||
requests_mock.assert_called_once_with("DELETE", pytest.helpers.anyvar(str, True),
|
|
||||||
params=web_client.repository_id.query() + [("keep_last_records", "42")])
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate_failed(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress any exception happened during logs rotation
|
|
||||||
"""
|
|
||||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
|
||||||
web_client.logs_rotate(42)
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate_failed_http_error(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress HTTP exception happened during logs rotation
|
|
||||||
"""
|
|
||||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
|
||||||
web_client.logs_rotate(42)
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate_failed_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress any exception happened during logs rotation and don't log
|
|
||||||
"""
|
|
||||||
web_client.suppress_errors = True
|
|
||||||
mocker.patch("requests.Session.request", side_effect=Exception())
|
|
||||||
logging_mock = mocker.patch("logging.exception")
|
|
||||||
|
|
||||||
web_client.logs_rotate(42)
|
|
||||||
logging_mock.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_logs_rotate_failed_http_error_suppress(web_client: WebClient, mocker: MockerFixture) -> None:
|
|
||||||
"""
|
|
||||||
must suppress HTTP exception happened during logs rotation and don't log
|
|
||||||
"""
|
|
||||||
web_client.suppress_errors = True
|
|
||||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError())
|
|
||||||
logging_mock = mocker.patch("logging.exception")
|
|
||||||
|
|
||||||
web_client.logs_rotate(42)
|
|
||||||
logging_mock.assert_not_called()
|
|
||||||
|
|
||||||
|
|
||||||
def test_package_changes_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_changes_get(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
"""
|
"""
|
||||||
must get changes
|
must get changes
|
||||||
@ -602,7 +551,6 @@ def test_package_logs_add(web_client: WebClient, log_record: logging.LogRecord,
|
|||||||
payload = {
|
payload = {
|
||||||
"created": log_record.created,
|
"created": log_record.created,
|
||||||
"message": log_record.getMessage(),
|
"message": log_record.getMessage(),
|
||||||
"process_id": LogRecordId.process_id,
|
|
||||||
"version": package_ahriman.version,
|
"version": package_ahriman.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -640,12 +588,7 @@ def test_package_logs_get(web_client: WebClient, package_ahriman: Package, mocke
|
|||||||
"""
|
"""
|
||||||
must get logs
|
must get logs
|
||||||
"""
|
"""
|
||||||
message = {
|
message = {"created": 42.0, "message": "log"}
|
||||||
"created": 42.0,
|
|
||||||
"message": "log",
|
|
||||||
"version": package_ahriman.version,
|
|
||||||
"process_id": LogRecordId.process_id,
|
|
||||||
}
|
|
||||||
response_obj = requests.Response()
|
response_obj = requests.Response()
|
||||||
response_obj._content = json.dumps([message]).encode("utf8")
|
response_obj._content = json.dumps([message]).encode("utf8")
|
||||||
response_obj.status_code = 200
|
response_obj.status_code = 200
|
||||||
@ -655,9 +598,7 @@ def test_package_logs_get(web_client: WebClient, package_ahriman: Package, mocke
|
|||||||
result = web_client.package_logs_get(package_ahriman.base, 1, 2)
|
result = web_client.package_logs_get(package_ahriman.base, 1, 2)
|
||||||
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
requests_mock.assert_called_once_with("GET", pytest.helpers.anyvar(str, True),
|
||||||
params=web_client.repository_id.query() + [("limit", "1"), ("offset", "2")])
|
params=web_client.repository_id.query() + [("limit", "1"), ("offset", "2")])
|
||||||
assert result == [
|
assert result == [(message["created"], message["message"])]
|
||||||
(LogRecordId(package_ahriman.base, package_ahriman.version), message["created"], message["message"]),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def test_package_logs_get_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
def test_package_logs_get_failed(web_client: WebClient, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||||
|
@ -479,7 +479,6 @@ def test_walk(resource_path_root: Path) -> None:
|
|||||||
resource_path_root / "models" / "pkgbuild",
|
resource_path_root / "models" / "pkgbuild",
|
||||||
resource_path_root / "models" / "utf8",
|
resource_path_root / "models" / "utf8",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "dashboard.jinja2",
|
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
|
||||||
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",
|
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",
|
||||||
|
@ -14,7 +14,6 @@ from ahriman.models.package_description import PackageDescription
|
|||||||
from ahriman.models.package_source import PackageSource
|
from ahriman.models.package_source import PackageSource
|
||||||
from ahriman.models.pkgbuild import Pkgbuild
|
from ahriman.models.pkgbuild import Pkgbuild
|
||||||
from ahriman.models.remote_source import RemoteSource
|
from ahriman.models.remote_source import RemoteSource
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -72,9 +71,8 @@ def internal_status(counters: Counters) -> InternalStatus:
|
|||||||
status=BuildStatus(),
|
status=BuildStatus(),
|
||||||
architecture="x86_64",
|
architecture="x86_64",
|
||||||
packages=counters,
|
packages=counters,
|
||||||
repository="aur",
|
|
||||||
stats=RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4),
|
|
||||||
version=__version__,
|
version=__version__,
|
||||||
|
repository="aur",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,10 +68,3 @@ def test_lt_invalid() -> None:
|
|||||||
"""
|
"""
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
assert RepositoryId("x86_64", "a") < 42
|
assert RepositoryId("x86_64", "a") < 42
|
||||||
|
|
||||||
|
|
||||||
def test_str() -> None:
|
|
||||||
"""
|
|
||||||
must convert identifier to string
|
|
||||||
"""
|
|
||||||
assert str(RepositoryId("x86_64", "a")) == "a (x86_64)"
|
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
from dataclasses import asdict
|
|
||||||
|
|
||||||
from ahriman.models.package import Package
|
|
||||||
from ahriman.models.repository_stats import RepositoryStats
|
|
||||||
|
|
||||||
|
|
||||||
def test_repository_stats_from_json_view(package_ahriman: Package, package_python_schedule: Package) -> None:
|
|
||||||
"""
|
|
||||||
must construct same object from json
|
|
||||||
"""
|
|
||||||
stats = RepositoryStats.from_packages([package_ahriman, package_python_schedule])
|
|
||||||
assert RepositoryStats.from_json(asdict(stats)) == stats
|
|
||||||
|
|
||||||
|
|
||||||
def test_from_packages(package_ahriman: Package, package_python_schedule: Package) -> None:
|
|
||||||
"""
|
|
||||||
must generate stats from packages list
|
|
||||||
"""
|
|
||||||
assert RepositoryStats.from_packages([package_ahriman, package_python_schedule]) == RepositoryStats(
|
|
||||||
bases=2,
|
|
||||||
packages=3,
|
|
||||||
archive_size=12603,
|
|
||||||
installed_size=12600003,
|
|
||||||
)
|
|
@ -1,80 +0,0 @@
|
|||||||
from ahriman.models.series_statistics import SeriesStatistics
|
|
||||||
|
|
||||||
|
|
||||||
def test_max() -> None:
|
|
||||||
"""
|
|
||||||
must return maximal value
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1, 3, 2]).max == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_max_empty() -> None:
|
|
||||||
"""
|
|
||||||
must return None as maximal value if series is empty
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([]).max is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_mean() -> None:
|
|
||||||
"""
|
|
||||||
must return mean value
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1, 3, 2]).mean == 2
|
|
||||||
|
|
||||||
|
|
||||||
def test_mean_empty() -> None:
|
|
||||||
"""
|
|
||||||
must return None as mean value if series is empty
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([]).mean is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_min() -> None:
|
|
||||||
"""
|
|
||||||
must return minimal value
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1, 3, 2]).min == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_min_empty() -> None:
|
|
||||||
"""
|
|
||||||
must return None as minimal value if series is empty
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([]).min is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_st_dev() -> None:
|
|
||||||
"""
|
|
||||||
must return standard deviation
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1, 3, 2]).st_dev == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_st_dev_empty() -> None:
|
|
||||||
"""
|
|
||||||
must return None as standard deviation if series is empty
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([]).st_dev is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_st_dev_single() -> None:
|
|
||||||
"""
|
|
||||||
must return 0 as standard deviation if series contains only one element
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1]).st_dev == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_total() -> None:
|
|
||||||
"""
|
|
||||||
must return size of collection
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1]).total == 1
|
|
||||||
assert SeriesStatistics([]).total == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_bool() -> None:
|
|
||||||
"""
|
|
||||||
must correctly define empty collection
|
|
||||||
"""
|
|
||||||
assert SeriesStatistics([1])
|
|
||||||
assert not SeriesStatistics([])
|
|
@ -1 +0,0 @@
|
|||||||
# schema testing goes in view class tests
|
|
@ -1,77 +0,0 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from aiohttp.test_utils import TestClient
|
|
||||||
|
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
|
||||||
from ahriman.models.package import Package
|
|
||||||
from ahriman.models.user_access import UserAccess
|
|
||||||
from ahriman.web.views.v1.service.logs import LogsView
|
|
||||||
|
|
||||||
|
|
||||||
async def test_get_permission() -> None:
|
|
||||||
"""
|
|
||||||
must return correct permission for the request
|
|
||||||
"""
|
|
||||||
for method in ("DELETE",):
|
|
||||||
request = pytest.helpers.request("", "", method)
|
|
||||||
assert await LogsView.get_permission(request) == UserAccess.Full
|
|
||||||
|
|
||||||
|
|
||||||
def test_routes() -> None:
|
|
||||||
"""
|
|
||||||
must return correct routes
|
|
||||||
"""
|
|
||||||
assert LogsView.ROUTES == ["/api/v1/service/logs"]
|
|
||||||
|
|
||||||
|
|
||||||
async def test_delete(client: TestClient, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must delete all logs
|
|
||||||
"""
|
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
|
||||||
json={"created": 42.0, "message": "message 1", "version": "42"})
|
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
|
||||||
json={"created": 43.0, "message": "message 2", "version": "43"})
|
|
||||||
|
|
||||||
response = await client.delete("/api/v1/service/logs")
|
|
||||||
assert response.status == 204
|
|
||||||
|
|
||||||
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs")
|
|
||||||
json = await response.json()
|
|
||||||
assert not json
|
|
||||||
|
|
||||||
|
|
||||||
async def test_delete_partially(client: TestClient, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must delete logs based on count input
|
|
||||||
"""
|
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}",
|
|
||||||
json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()})
|
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
|
||||||
json={"created": 42.0, "message": "message 1", "version": "42"})
|
|
||||||
await client.post(f"/api/v1/packages/{package_ahriman.base}/logs",
|
|
||||||
json={"created": 43.0, "message": "message 2", "version": "43"})
|
|
||||||
request_schema = pytest.helpers.schema_request(LogsView.delete, location="querystring")
|
|
||||||
|
|
||||||
payload = {"keep_last_records": 1}
|
|
||||||
assert not request_schema.validate(payload)
|
|
||||||
|
|
||||||
response = await client.delete("/api/v1/service/logs", params=payload)
|
|
||||||
assert response.status == 204
|
|
||||||
|
|
||||||
response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs")
|
|
||||||
json = await response.json()
|
|
||||||
assert json
|
|
||||||
|
|
||||||
|
|
||||||
async def test_delete_exception(client: TestClient, package_ahriman: Package) -> None:
|
|
||||||
"""
|
|
||||||
must raise exception on invalid payload
|
|
||||||
"""
|
|
||||||
response_schema = pytest.helpers.schema_response(LogsView.delete, code=400)
|
|
||||||
|
|
||||||
response = await client.delete("/api/v1/service/logs", params={"keep_last_records": "string"})
|
|
||||||
assert response.status == 400
|
|
||||||
assert not response_schema.validate(await response.json())
|
|
@ -3,7 +3,6 @@ import pytest
|
|||||||
from aiohttp.test_utils import TestClient
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
from ahriman.models.build_status import BuildStatusEnum
|
from ahriman.models.build_status import BuildStatusEnum
|
||||||
from ahriman.models.log_record_id import LogRecordId
|
|
||||||
from ahriman.models.package import Package
|
from ahriman.models.package import Package
|
||||||
from ahriman.models.user_access import UserAccess
|
from ahriman.models.user_access import UserAccess
|
||||||
from ahriman.web.views.v2.packages.logs import LogsView
|
from ahriman.web.views.v2.packages.logs import LogsView
|
||||||
@ -49,14 +48,10 @@ async def test_get(client: TestClient, package_ahriman: Package) -> None:
|
|||||||
{
|
{
|
||||||
"created": 42.0,
|
"created": 42.0,
|
||||||
"message": "message 1",
|
"message": "message 1",
|
||||||
"version": "42",
|
|
||||||
"process_id": LogRecordId.process_id,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"created": 43.0,
|
"created": 43.0,
|
||||||
"message": "message 2",
|
"message": "message 2",
|
||||||
"version": "42",
|
|
||||||
"process_id": LogRecordId.process_id,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -81,7 +76,7 @@ async def test_get_with_pagination(client: TestClient, package_ahriman: Package)
|
|||||||
|
|
||||||
logs = await response.json()
|
logs = await response.json()
|
||||||
assert not response_schema.validate(logs)
|
assert not response_schema.validate(logs)
|
||||||
assert logs == [{"created": 42.0, "message": "message 1", "version": "42", "process_id": LogRecordId.process_id}]
|
assert logs == [{"created": 42.0, "message": "message 1"}]
|
||||||
|
|
||||||
|
|
||||||
async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None:
|
async def test_get_bad_request(client: TestClient, package_ahriman: Package) -> None:
|
||||||
|
Reference in New Issue
Block a user