mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-07-16 07:19:57 +00:00
Compare commits
11 Commits
2.17.0
...
9a23f5c79d
Author | SHA1 | Date | |
---|---|---|---|
9a23f5c79d | |||
aaab9069bf | |||
f00b575641 | |||
6f57ed550b | |||
08640d9108 | |||
65324633b4 | |||
ed67898012 | |||
a1a8dd68e8 | |||
a9505386c2 | |||
a07b20bf50 | |||
ed70897c39 |
@ -80,7 +80,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
>>> clazz = Clazz()
|
||||
"""
|
||||
|
||||
CLAZZ_ATTRIBUTE = 42
|
||||
CLAZZ_ATTRIBUTE: ClassVar[int] = 42
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
@ -96,6 +96,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
|
||||
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typing.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
||||
* Class attributes must be decorated as `ClassVar[...]`.
|
||||
* Recommended order of function definitions in class:
|
||||
|
||||
```python
|
||||
|
@ -124,6 +124,14 @@ ahriman.core.database.migrations.m014\_auditlog module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.core.database.migrations.m015\_logs\_process\_id module
|
||||
---------------------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.core.database.migrations.m015_logs_process_id
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
|
@ -92,6 +92,14 @@ ahriman.core.formatters.repository\_printer module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
----------------------------------------------
|
||||
|
||||
|
@ -100,6 +100,14 @@ ahriman.models.log\_handler module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.log\_record module
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.log_record
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.log\_record\_id module
|
||||
-------------------------------------
|
||||
|
||||
@ -236,6 +244,14 @@ ahriman.models.repository\_paths module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.repository\_stats module
|
||||
---------------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.repository_stats
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.result module
|
||||
----------------------------
|
||||
|
||||
@ -252,6 +268,14 @@ ahriman.models.scan\_paths module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.series\_statistics module
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: ahriman.models.series_statistics
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.models.sign\_settings module
|
||||
------------------------------------
|
||||
|
||||
|
@ -116,6 +116,14 @@ ahriman.web.schemas.login\_schema module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.logs\_rotate\_schema module
|
||||
-----------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.schemas.logs_rotate_schema
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.logs\_schema module
|
||||
---------------------------------------
|
||||
|
||||
@ -260,6 +268,14 @@ ahriman.web.schemas.repository\_id\_schema module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
-----------------------------------------
|
||||
|
||||
@ -284,14 +300,6 @@ ahriman.web.schemas.update\_flags\_schema module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.versioned\_log\_schema module
|
||||
-------------------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.schemas.versioned_log_schema
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.schemas.worker\_schema module
|
||||
-----------------------------------------
|
||||
|
||||
|
@ -12,6 +12,14 @@ ahriman.web.views.v1.service.add module
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.views.v1.service.logs module
|
||||
----------------------------------------
|
||||
|
||||
.. automodule:: ahriman.web.views.v1.service.logs
|
||||
:members:
|
||||
:no-undoc-members:
|
||||
:show-inheritance:
|
||||
|
||||
ahriman.web.views.v1.service.pgp module
|
||||
---------------------------------------
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
pkgbase='ahriman'
|
||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||
pkgver=2.17.0
|
||||
pkgver=2.17.1
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
|
@ -7,6 +7,8 @@ logging = ahriman.ini.d/logging.ini
|
||||
;apply_migrations = yes
|
||||
; Path to the application SQLite database.
|
||||
database = ${repository:root}/ahriman.db
|
||||
; Keep last build logs for each package
|
||||
keep_last_logs = 5
|
||||
|
||||
[alpm]
|
||||
; Path to pacman system database cache.
|
||||
|
@ -16,11 +16,11 @@
|
||||
<div class="container">
|
||||
<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>
|
||||
<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">
|
||||
<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">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div id="repositories-navbar-supported-content" class="collapse navbar-collapse">
|
||||
<div id="repositories-navbar" class="collapse navbar-collapse">
|
||||
<ul id="repositories" class="nav nav-tabs">
|
||||
{% for repository in repositories %}
|
||||
<li class="nav-item">
|
||||
@ -36,7 +36,9 @@
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar" class="dropdown">
|
||||
<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>
|
||||
<button id="dashboard-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#dashboard-modal">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</button>
|
||||
|
||||
{% 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">
|
||||
@ -152,6 +154,7 @@
|
||||
|
||||
{% include "build-status/alerts.jinja2" %}
|
||||
|
||||
{% include "build-status/dashboard.jinja2" %}
|
||||
{% include "build-status/package-add-modal.jinja2" %}
|
||||
{% include "build-status/package-rebuild-modal.jinja2" %}
|
||||
{% include "build-status/key-import-modal.jinja2" %}
|
||||
|
@ -0,0 +1,76 @@
|
||||
<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,7 +59,14 @@
|
||||
</nav>
|
||||
<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">
|
||||
<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="row">
|
||||
<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 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>
|
||||
@ -100,6 +107,7 @@
|
||||
const packageInfoModalHeader = document.getElementById("package-info-modal-header");
|
||||
const packageInfo = document.getElementById("package-info");
|
||||
|
||||
const packageInfoLogsVersions = document.getElementById("package-info-logs-versions");
|
||||
const packageInfoLogsInput = document.getElementById("package-info-logs-input");
|
||||
const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button");
|
||||
|
||||
@ -285,25 +293,51 @@
|
||||
convert: response => response.json(),
|
||||
},
|
||||
data => {
|
||||
const logs = data.map(log_record => {
|
||||
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
|
||||
});
|
||||
packageInfoLogsInput.textContent = logs.join("\n");
|
||||
highlight(packageInfoLogsInput);
|
||||
const selectors = Object
|
||||
.values(
|
||||
data.reduce((acc, log_record) => {
|
||||
const id = `${log_record.version}-${log_record.process_id}`;
|
||||
if (acc[id])
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
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(
|
||||
`/api/v1/packages/${packageBase}`,
|
||||
{
|
||||
|
@ -7,7 +7,7 @@
|
||||
// so far bootstrap-table only operates with jquery elements
|
||||
const table = $(document.getElementById("packages"));
|
||||
|
||||
const statusBadge = document.getElementById("badge-status");
|
||||
const dashboardButton = document.getElementById("dashboard-button");
|
||||
const versionBadge = document.getElementById("badge-version");
|
||||
|
||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||
@ -141,14 +141,62 @@
|
||||
data => {
|
||||
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
||||
|
||||
statusBadge.classList.remove(...statusBadge.classList);
|
||||
statusBadge.classList.add("btn");
|
||||
statusBadge.classList.add(badgeClass(data.status.status));
|
||||
dashboardButton.classList.remove(...dashboardButton.classList);
|
||||
dashboardButton.classList.add("btn");
|
||||
dashboardButton.classList.add(badgeClass(data.status.status));
|
||||
|
||||
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||
popover.dispose();
|
||||
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`;
|
||||
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
|
||||
dashboardModalHeader.classList.add("modal-header");
|
||||
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
|
||||
|
||||
dashboardName.textContent = data.repository;
|
||||
dashboardArchitecture.textContent = data.architecture;
|
||||
dashboardStatus.textContent = data.status.status;
|
||||
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
|
||||
|
||||
if (dashboardPackagesStatusesChart) {
|
||||
const labels = [
|
||||
"unknown",
|
||||
"pending",
|
||||
"building",
|
||||
"failed",
|
||||
"success",
|
||||
];
|
||||
dashboardPackagesStatusesChart.config.data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: "packages in status",
|
||||
data: labels.map(label => data.packages[label]),
|
||||
backgroundColor: [
|
||||
"rgb(55, 58, 60)",
|
||||
"rgb(255, 117, 24)",
|
||||
"rgb(255, 117, 24)",
|
||||
"rgb(255, 0, 57)",
|
||||
"rgb(63, 182, 24)", // copy-paste from current style
|
||||
],
|
||||
}],
|
||||
};
|
||||
dashboardPackagesStatusesChart.update();
|
||||
}
|
||||
|
||||
if (dashboardPackagesCountChart) {
|
||||
dashboardPackagesCountChart.config.data = {
|
||||
labels: ["packages"],
|
||||
datasets: [
|
||||
{
|
||||
label: "archives",
|
||||
data: [data.stats.packages],
|
||||
},
|
||||
{
|
||||
label: "bases",
|
||||
data: [data.stats.bases],
|
||||
},
|
||||
],
|
||||
};
|
||||
dashboardPackagesCountChart.update();
|
||||
}
|
||||
|
||||
dashboardCanvas.hidden = data.status.total > 0;
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -227,7 +275,6 @@
|
||||
});
|
||||
});
|
||||
|
||||
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||
selectRepository();
|
||||
});
|
||||
</script>
|
||||
|
@ -58,6 +58,14 @@
|
||||
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) {
|
||||
return Array.from(new Set(data))
|
||||
.sort()
|
||||
|
@ -27,4 +27,10 @@
|
||||
top: 0;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
color: black !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2024\-12\-29" "ahriman" "Generated Python Manual"
|
||||
.TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual"
|
||||
.SH NAME
|
||||
ahriman
|
||||
.SH SYNOPSIS
|
||||
|
@ -17,4 +17,4 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "2.17.0"
|
||||
__version__ = "2.17.1"
|
||||
|
@ -22,7 +22,7 @@ import logging
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from multiprocessing import Pool
|
||||
from typing import TypeVar
|
||||
from typing import ClassVar, TypeVar
|
||||
|
||||
from ahriman.application.lock import Lock
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -58,8 +58,8 @@ class Handler:
|
||||
>>> Add.execute(args)
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = True
|
||||
arguments: list[Callable[[SubParserAction], argparse.ArgumentParser]]
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN: ClassVar[bool] = True
|
||||
arguments: ClassVar[list[Callable[[SubParserAction], argparse.ArgumentParser]]]
|
||||
|
||||
@classmethod
|
||||
def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool:
|
||||
|
@ -21,6 +21,7 @@ import argparse
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import fields
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.alpm.remote import AUR, Official
|
||||
@ -40,7 +41,7 @@ class Search(Handler):
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||
SORT_FIELDS = {
|
||||
SORT_FIELDS: ClassVar[set[str]] = {
|
||||
field.name
|
||||
for field in fields(AURPackage)
|
||||
if field.default_factory is not list
|
||||
|
@ -21,6 +21,7 @@ import argparse
|
||||
|
||||
from pathlib import Path
|
||||
from pwd import getpwuid
|
||||
from typing import ClassVar
|
||||
from urllib.parse import quote_plus as url_encode
|
||||
|
||||
from ahriman.application.application import Application
|
||||
@ -46,9 +47,9 @@ class Setup(Handler):
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||
|
||||
ARCHBUILD_COMMAND_PATH = Path("/") / "usr" / "bin" / "archbuild"
|
||||
MIRRORLIST_PATH = Path("/") / "etc" / "pacman.d" / "mirrorlist"
|
||||
SUDOERS_DIR_PATH = Path("/") / "etc" / "sudoers.d"
|
||||
ARCHBUILD_COMMAND_PATH: ClassVar[Path] = Path("/") / "usr" / "bin" / "archbuild"
|
||||
MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist"
|
||||
SUDOERS_DIR_PATH: ClassVar[Path] = Path("/") / "etc" / "sudoers.d"
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
|
@ -27,7 +27,7 @@ from pathlib import Path
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter
|
||||
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter, RepositoryStatsPrinter
|
||||
from ahriman.core.utils import enum_values, pretty_datetime
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
@ -64,6 +64,7 @@ class Statistics(Handler):
|
||||
|
||||
match args.package:
|
||||
case None:
|
||||
RepositoryStatsPrinter(repository_id, application.reporter.statistics())(verbose=True)
|
||||
Statistics.stats_per_package(args.event, events, args.chart)
|
||||
case _:
|
||||
Statistics.stats_for_package(args.event, events, args.chart)
|
||||
|
@ -23,6 +23,7 @@ import sys
|
||||
|
||||
from collections.abc import Generator
|
||||
from importlib import metadata
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman import __version__
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
@ -36,11 +37,11 @@ class Versions(Handler):
|
||||
version handler
|
||||
|
||||
Attributes:
|
||||
PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name
|
||||
PEP423_PACKAGE_NAME(re.Pattern[str]): (class attribute) special regex for valid PEP423 package name
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+")
|
||||
PEP423_PACKAGE_NAME: ClassVar[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9._-]+")
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from code import InteractiveConsole
|
||||
from importlib.util import find_spec
|
||||
from typing import Any
|
||||
|
||||
|
||||
@ -26,6 +27,19 @@ class InteractiveShell(InteractiveConsole):
|
||||
wrapper around :class:`code.InteractiveConsole` to pass :func:`interact()` to IPython shell
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def has_ipython() -> bool:
|
||||
"""
|
||||
check if IPython shell is available
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if IPython shell is available, ``False`` otherwise
|
||||
"""
|
||||
try:
|
||||
return find_spec("IPython.terminal.embed") is not None
|
||||
except ModuleNotFoundError:
|
||||
return False
|
||||
|
||||
def interact(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
pass controller to IPython shell
|
||||
@ -34,13 +48,13 @@ class InteractiveShell(InteractiveConsole):
|
||||
*args(Any): positional arguments
|
||||
**kwargs(Any): keyword arguments
|
||||
"""
|
||||
try:
|
||||
if self.has_ipython():
|
||||
from IPython.terminal.embed import InteractiveShellEmbed
|
||||
|
||||
shell = InteractiveShellEmbed(user_ns=self.locals) # type: ignore[no-untyped-call]
|
||||
shell.show_banner() # type: ignore[no-untyped-call]
|
||||
shell.interact() # type: ignore[no-untyped-call]
|
||||
except ImportError:
|
||||
else:
|
||||
# fallback to default
|
||||
import readline # pylint: disable=unused-import
|
||||
InteractiveConsole.interact(self, *args, **kwargs)
|
||||
|
@ -23,6 +23,7 @@ import shutil
|
||||
from email.utils import parsedate_to_datetime
|
||||
from pathlib import Path
|
||||
from pyalpm import DB # type: ignore[import-not-found]
|
||||
from typing import ClassVar
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -41,7 +42,7 @@ class PacmanDatabase(SyncHttpClient):
|
||||
sync_files_database(bool): sync files database
|
||||
"""
|
||||
|
||||
LAST_MODIFIED_HEADER = "Last-Modified"
|
||||
LAST_MODIFIED_HEADER: ClassVar[str] = "Last-Modified"
|
||||
|
||||
def __init__(self, database: DB, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
@ -34,14 +34,14 @@ class PkgbuildToken(StrEnum):
|
||||
well-known tokens dictionary
|
||||
|
||||
Attributes:
|
||||
ArrayEnds(PkgbuildToken): (class attribute) array ends token
|
||||
ArrayStarts(PkgbuildToken): (class attribute) array starts token
|
||||
Comma(PkgbuildToken): (class attribute) comma token
|
||||
Comment(PkgbuildToken): (class attribute) comment token
|
||||
FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token
|
||||
FunctionEnds(PkgbuildToken): (class attribute) function ends token
|
||||
FunctionStarts(PkgbuildToken): (class attribute) function starts token
|
||||
NewLine(PkgbuildToken): (class attribute) new line token
|
||||
ArrayEnds(PkgbuildToken): array ends token
|
||||
ArrayStarts(PkgbuildToken): array starts token
|
||||
Comma(PkgbuildToken): comma token
|
||||
Comment(PkgbuildToken): comment token
|
||||
FunctionDeclaration(PkgbuildToken): function declaration token
|
||||
FunctionEnds(PkgbuildToken): function ends token
|
||||
FunctionStarts(PkgbuildToken): function starts token
|
||||
NewLine(PkgbuildToken): new line token
|
||||
"""
|
||||
|
||||
ArrayStarts = "("
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote.remote import Remote
|
||||
@ -35,9 +35,9 @@ class AUR(Remote):
|
||||
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
|
||||
"""
|
||||
|
||||
DEFAULT_AUR_URL = "https://aur.archlinux.org"
|
||||
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
|
||||
DEFAULT_RPC_VERSION = "5"
|
||||
DEFAULT_AUR_URL: ClassVar[str] = "https://aur.archlinux.org"
|
||||
DEFAULT_RPC_URL: ClassVar[str] = f"{DEFAULT_AUR_URL}/rpc"
|
||||
DEFAULT_RPC_VERSION: ClassVar[str] = "5"
|
||||
|
||||
@classmethod
|
||||
def remote_git_url(cls, package_base: str, repository: str) -> str:
|
||||
|
@ -17,7 +17,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote.remote import Remote
|
||||
@ -36,10 +36,10 @@ class Official(Remote):
|
||||
DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url
|
||||
"""
|
||||
|
||||
DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org"
|
||||
DEFAULT_ARCHLINUX_URL = "https://archlinux.org"
|
||||
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"]
|
||||
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
|
||||
DEFAULT_ARCHLINUX_GIT_URL: ClassVar[str] = "https://gitlab.archlinux.org"
|
||||
DEFAULT_ARCHLINUX_URL: ClassVar[str] = "https://archlinux.org"
|
||||
DEFAULT_SEARCH_REPOSITORIES: ClassVar[list[str]] = ["Core", "Extra", "Multilib"]
|
||||
DEFAULT_RPC_URL: ClassVar[str] = "https://archlinux.org/packages/search/json"
|
||||
|
||||
@classmethod
|
||||
def remote_git_url(cls, package_base: str, repository: str) -> str:
|
||||
|
@ -21,6 +21,7 @@ import shutil
|
||||
|
||||
from collections.abc import Generator
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.exceptions import CalledProcessError
|
||||
from ahriman.core.log import LazyLogging
|
||||
@ -42,9 +43,9 @@ class Sources(LazyLogging):
|
||||
GITCONFIG(dict[str, str]): (class attribute) git config options to suppress annoying hints
|
||||
"""
|
||||
|
||||
DEFAULT_BRANCH = "master" # default fallback branch
|
||||
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
|
||||
GITCONFIG = {
|
||||
DEFAULT_BRANCH: ClassVar[str] = "master" # default fallback branch
|
||||
DEFAULT_COMMIT_AUTHOR: ClassVar[tuple[str, str]] = ("ahriman", "ahriman@localhost")
|
||||
GITCONFIG: ClassVar[dict[str, str]] = {
|
||||
"init.defaultBranch": DEFAULT_BRANCH,
|
||||
}
|
||||
|
||||
|
@ -149,8 +149,11 @@ class Task(LazyLogging):
|
||||
str | None: current commit sha if available
|
||||
"""
|
||||
last_commit_sha = Sources.load(sources_dir, self.package, patches, self.paths)
|
||||
if local_version is None:
|
||||
return last_commit_sha # there is no local package or pkgrel increment is disabled
|
||||
if self.package.is_vcs: # if package is VCS, then make sure to update PKGBUILD to the latest version
|
||||
self.build(sources_dir, dry_run=True)
|
||||
|
||||
if local_version is None: # there is no local package or pkgrel increment is disabled
|
||||
return last_commit_sha
|
||||
|
||||
# load fresh package
|
||||
loaded_package = Package.from_build(sources_dir, self.architecture, None)
|
||||
|
@ -22,7 +22,7 @@ import shlex
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
from typing import Any, ClassVar, Self
|
||||
|
||||
from ahriman.core.configuration.configuration_multi_dict import ConfigurationMultiDict
|
||||
from ahriman.core.configuration.shell_interpolator import ShellInterpolator
|
||||
@ -65,8 +65,8 @@ class Configuration(configparser.RawConfigParser):
|
||||
"""
|
||||
|
||||
_LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS = ["web"]
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"]
|
||||
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
|
||||
ARCHITECTURE_SPECIFIC_SECTIONS: ClassVar[list[str]] = ["alpm", "build", "sign"]
|
||||
SYSTEM_CONFIGURATION_PATH: ClassVar[Path] = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
|
||||
|
||||
def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None:
|
||||
"""
|
||||
|
@ -57,7 +57,7 @@ class ConfigurationMultiDict(dict[str, Any]):
|
||||
OptionError: if the key already exists in the dictionary, but not a single value list or a string
|
||||
"""
|
||||
match self.get(key):
|
||||
case [current_value] | str(current_value): # type: ignore[misc]
|
||||
case [current_value] | str(current_value):
|
||||
value = f"{current_value} {value}"
|
||||
case None:
|
||||
pass
|
||||
|
@ -45,6 +45,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
"path_exists": True,
|
||||
"path_type": "dir",
|
||||
},
|
||||
"keep_last_logs": {
|
||||
"type": "integer",
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
"logging": {
|
||||
"type": "path",
|
||||
"coerce": "absolute_path",
|
||||
|
@ -23,6 +23,7 @@ import sys
|
||||
|
||||
from collections.abc import Generator, Mapping, MutableMapping
|
||||
from string import Template
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration.shell_template import ShellTemplate
|
||||
|
||||
@ -32,7 +33,7 @@ class ShellInterpolator(configparser.Interpolation):
|
||||
custom string interpolator, because we cannot use defaults argument due to config validation
|
||||
"""
|
||||
|
||||
DATA_LINK_ESCAPE = "\x10"
|
||||
DATA_LINK_ESCAPE: ClassVar[str] = "\x10"
|
||||
|
||||
@staticmethod
|
||||
def _extract_variables(parser: MutableMapping[str, Mapping[str, str]], value: str,
|
||||
|
@ -28,9 +28,6 @@ class ShellTemplate(Template):
|
||||
"""
|
||||
extension to the default :class:`Template` class, which also adds additional tokens to braced regex and enables
|
||||
bash expansion
|
||||
|
||||
Attributes:
|
||||
braceidpattern(str): regular expression to match every character except for closing bracket
|
||||
"""
|
||||
|
||||
braceidpattern = r"(?a:[_a-z0-9][^}]*)"
|
||||
|
@ -62,24 +62,31 @@ class Migrations(LazyLogging):
|
||||
"""
|
||||
return Migrations(connection, configuration).run()
|
||||
|
||||
def migration(self, cursor: Cursor, migration: Migration) -> None:
|
||||
def apply_migrations(self, migrations: list[Migration]) -> None:
|
||||
"""
|
||||
perform single migration
|
||||
perform migrations explicitly
|
||||
|
||||
Args:
|
||||
cursor(Cursor): connection cursor
|
||||
migration(Migration): single migration to perform
|
||||
migrations(list[Migration]): list of migrations to perform
|
||||
"""
|
||||
self.logger.info("applying table migration %s at index %s", migration.name, migration.index)
|
||||
for statement in migration.steps:
|
||||
cursor.execute(statement)
|
||||
self.logger.info("table migration %s at index %s has been applied", migration.name, migration.index)
|
||||
|
||||
self.logger.info("perform data migration %s at index %s", migration.name, migration.index)
|
||||
migration.migrate_data(self.connection, self.configuration)
|
||||
self.logger.info(
|
||||
"data migration %s at index %s has been performed",
|
||||
migration.name, migration.index)
|
||||
previous_isolation = self.connection.isolation_level
|
||||
try:
|
||||
self.connection.isolation_level = None
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("begin exclusive")
|
||||
for migration in migrations:
|
||||
self.perform_migration(cursor, migration)
|
||||
except Exception:
|
||||
self.logger.exception("migration failed with exception")
|
||||
cursor.execute("rollback")
|
||||
raise
|
||||
else:
|
||||
cursor.execute("commit")
|
||||
finally:
|
||||
cursor.close()
|
||||
finally:
|
||||
self.connection.isolation_level = previous_isolation
|
||||
|
||||
def migrations(self) -> list[Migration]:
|
||||
"""
|
||||
@ -114,6 +121,25 @@ class Migrations(LazyLogging):
|
||||
|
||||
return migrations
|
||||
|
||||
def perform_migration(self, cursor: Cursor, migration: Migration) -> None:
|
||||
"""
|
||||
perform single migration
|
||||
|
||||
Args:
|
||||
cursor(Cursor): connection cursor
|
||||
migration(Migration): single migration to perform
|
||||
"""
|
||||
self.logger.info("applying table migration %s at index %s", migration.name, migration.index)
|
||||
for statement in migration.steps:
|
||||
cursor.execute(statement)
|
||||
self.logger.info("table migration %s at index %s has been applied", migration.name, migration.index)
|
||||
|
||||
self.logger.info("perform data migration %s at index %s", migration.name, migration.index)
|
||||
migration.migrate_data(self.connection, self.configuration)
|
||||
self.logger.info(
|
||||
"data migration %s at index %s has been performed",
|
||||
migration.name, migration.index)
|
||||
|
||||
def run(self) -> MigrationResult:
|
||||
"""
|
||||
perform migrations
|
||||
@ -122,6 +148,7 @@ class Migrations(LazyLogging):
|
||||
MigrationResult: current schema version
|
||||
"""
|
||||
migrations = self.migrations()
|
||||
|
||||
current_version = self.user_version()
|
||||
expected_version = len(migrations)
|
||||
result = MigrationResult(old_version=current_version, new_version=expected_version)
|
||||
@ -130,25 +157,8 @@ class Migrations(LazyLogging):
|
||||
self.logger.info("no migrations required")
|
||||
return result
|
||||
|
||||
previous_isolation = self.connection.isolation_level
|
||||
try:
|
||||
self.connection.isolation_level = None
|
||||
cursor = self.connection.cursor()
|
||||
try:
|
||||
cursor.execute("begin exclusive")
|
||||
for migration in migrations[current_version:]:
|
||||
self.migration(cursor, migration)
|
||||
cursor.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
|
||||
except Exception:
|
||||
self.logger.exception("migration failed with exception")
|
||||
cursor.execute("rollback")
|
||||
raise
|
||||
else:
|
||||
cursor.execute("commit")
|
||||
finally:
|
||||
cursor.close()
|
||||
finally:
|
||||
self.connection.isolation_level = previous_isolation
|
||||
self.apply_migrations(migrations[current_version:])
|
||||
self.connection.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
|
||||
|
||||
self.logger.info("migrations have been performed from version %s to %s", result.old_version, result.new_version)
|
||||
return result
|
||||
|
30
src/ahriman/core/database/migrations/m015_logs_process_id.py
Normal file
30
src/ahriman/core/database/migrations/m015_logs_process_id.py
Normal file
@ -0,0 +1,30 @@
|
||||
#
|
||||
# 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 ''
|
||||
""",
|
||||
"""
|
||||
alter table logs rename column record to message
|
||||
""",
|
||||
]
|
@ -20,7 +20,7 @@
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations.operations import Operations
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@ class LogsOperations(Operations):
|
||||
"""
|
||||
|
||||
def logs_get(self, package_base: str, limit: int = -1, offset: int = 0,
|
||||
repository_id: RepositoryId | None = None) -> list[tuple[float, str]]:
|
||||
repository_id: RepositoryId | None = None) -> list[LogRecord]:
|
||||
"""
|
||||
extract logs for specified package base
|
||||
|
||||
@ -41,16 +41,16 @@ class LogsOperations(Operations):
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Return:
|
||||
list[tuple[float, str]]: sorted package log records and their timestamps
|
||||
list[LogRecord]: sorted package log records
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> list[tuple[float, str]]:
|
||||
def run(connection: Connection) -> list[LogRecord]:
|
||||
return [
|
||||
(row["created"], row["record"])
|
||||
LogRecord.from_json(package_base, row)
|
||||
for row in connection.execute(
|
||||
"""
|
||||
select created, record from (
|
||||
select created, message, version, process_id from (
|
||||
select * from logs
|
||||
where package_base = :package_base and repository = :repository
|
||||
order by created desc limit :limit offset :offset
|
||||
@ -66,15 +66,12 @@ class LogsOperations(Operations):
|
||||
|
||||
return self.with_connection(run)
|
||||
|
||||
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str,
|
||||
repository_id: RepositoryId | None = None) -> None:
|
||||
def logs_insert(self, log_record: LogRecord, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
write new log record to database
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): current log record id
|
||||
created(float): log created timestamp from log record attribute
|
||||
record(str): log record
|
||||
log_record(LogRecord): log record object
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
@ -83,17 +80,14 @@ class LogsOperations(Operations):
|
||||
connection.execute(
|
||||
"""
|
||||
insert into logs
|
||||
(package_base, version, created, record, repository)
|
||||
(package_base, version, created, message, repository, process_id)
|
||||
values
|
||||
(:package_base, :version, :created, :record, :repository)
|
||||
(:package_base, :version, :created, :message, :repository, :process_id)
|
||||
""",
|
||||
{
|
||||
"package_base": log_record_id.package_base,
|
||||
"version": log_record_id.version,
|
||||
"created": created,
|
||||
"record": record,
|
||||
"package_base": log_record.log_record_id.package_base,
|
||||
"repository": repository_id.id,
|
||||
}
|
||||
} | log_record.view()
|
||||
)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
@ -125,3 +119,54 @@ class LogsOperations(Operations):
|
||||
)
|
||||
|
||||
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)
|
||||
|
@ -22,7 +22,6 @@ import contextlib
|
||||
from functools import cached_property
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.configuration.schema import ConfigurationSchema
|
||||
from ahriman.core.status.web_client import WebClient
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
@ -34,7 +33,7 @@ class DistributedSystem(Trigger, WebClient):
|
||||
simple class to (un)register itself as a distributed worker
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA: ConfigurationSchema = {
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"worker": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
|
@ -95,6 +95,19 @@ class DuplicateRunError(RuntimeError):
|
||||
self, "Another application instance is run. This error can be suppressed by using --force flag.")
|
||||
|
||||
|
||||
class EncodeError(ValueError):
|
||||
"""
|
||||
exception used for bytes encoding errors
|
||||
"""
|
||||
|
||||
def __init__(self, encodings: list[str]) -> None:
|
||||
"""
|
||||
Args:
|
||||
encodings(list[str]): list of encodings tried
|
||||
"""
|
||||
ValueError.__init__(self, f"Could not encode bytes by using {encodings}")
|
||||
|
||||
|
||||
class ExitCode(RuntimeError):
|
||||
"""
|
||||
special exception which has to be thrown to return non-zero status without error message
|
||||
|
@ -28,6 +28,7 @@ from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||
from ahriman.core.formatters.printer import Printer
|
||||
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.string_printer import StringPrinter
|
||||
from ahriman.core.formatters.tree_printer import TreePrinter
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
@ -31,7 +33,7 @@ class ConfigurationPrinter(StringPrinter):
|
||||
values(dict[str, str]): configuration values dictionary
|
||||
"""
|
||||
|
||||
HIDE_KEYS = [
|
||||
HIDE_KEYS: ClassVar[list[str]] = [
|
||||
"api_key", # telegram key
|
||||
"client_secret", # oauth secret
|
||||
"cookie_secret_key", # cookie secret key
|
||||
|
@ -17,11 +17,9 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import statistics
|
||||
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.core.utils import minmax
|
||||
from ahriman.models.property import Property
|
||||
from ahriman.models.series_statistics import SeriesStatistics
|
||||
|
||||
|
||||
class EventStatsPrinter(StringPrinter):
|
||||
@ -29,7 +27,7 @@ class EventStatsPrinter(StringPrinter):
|
||||
print event statistics
|
||||
|
||||
Attributes:
|
||||
events(list[float | int]): event values to build statistics
|
||||
statistics(SeriesStatistics): statistics object
|
||||
"""
|
||||
|
||||
def __init__(self, event_type: str, events: list[float | int]) -> None:
|
||||
@ -39,7 +37,7 @@ class EventStatsPrinter(StringPrinter):
|
||||
events(list[float | int]): event values to build statistics
|
||||
"""
|
||||
StringPrinter.__init__(self, event_type)
|
||||
self.events = events
|
||||
self.statistics = SeriesStatistics(events)
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
"""
|
||||
@ -49,24 +47,17 @@ class EventStatsPrinter(StringPrinter):
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
properties = [
|
||||
Property("total", len(self.events)),
|
||||
Property("total", self.statistics.total),
|
||||
]
|
||||
|
||||
# time statistics
|
||||
if self.events:
|
||||
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}"
|
||||
if self.statistics:
|
||||
mean = self.statistics.mean
|
||||
|
||||
properties.extend([
|
||||
Property("min", min_time),
|
||||
Property("average", average),
|
||||
Property("max", max_time),
|
||||
Property("min", self.statistics.min),
|
||||
Property("average", f"{mean:.3f} ± {self.statistics.st_dev:.3f}"),
|
||||
Property("max", self.statistics.max),
|
||||
])
|
||||
|
||||
return properties
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.models.property import Property
|
||||
|
||||
@ -26,10 +28,11 @@ class PackageStatsPrinter(StringPrinter):
|
||||
print packages statistics
|
||||
|
||||
Attributes:
|
||||
MAX_COUNT(int): (class attribute) maximum number of packages to print
|
||||
events(dict[str, int]): map of package to its event frequency
|
||||
"""
|
||||
|
||||
MAX_COUNT = 10
|
||||
MAX_COUNT: ClassVar[int] = 10
|
||||
|
||||
def __init__(self, events: dict[str, int]) -> None:
|
||||
"""
|
||||
|
53
src/ahriman/core/formatters/repository_stats_printer.py
Normal file
53
src/ahriman/core/formatters/repository_stats_printer.py
Normal file
@ -0,0 +1,53 @@
|
||||
#
|
||||
# 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,12 +17,16 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import atexit
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from typing import Self
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
@ -33,6 +37,7 @@ class HttpLogHandler(logging.Handler):
|
||||
method
|
||||
|
||||
Attributes:
|
||||
keep_last_records(int): number of last records to keep
|
||||
reporter(Client): build status reporter instance
|
||||
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
|
||||
"""
|
||||
@ -51,6 +56,7 @@ class HttpLogHandler(logging.Handler):
|
||||
|
||||
self.reporter = Client.load(repository_id, configuration, report=report)
|
||||
self.suppress_errors = suppress_errors
|
||||
self.keep_last_records = configuration.getint("settings", "keep_last_logs", fallback=0)
|
||||
|
||||
@classmethod
|
||||
def load(cls, repository_id: RepositoryId, configuration: Configuration, *, report: bool) -> Self:
|
||||
@ -76,6 +82,9 @@ class HttpLogHandler(logging.Handler):
|
||||
handler = cls(repository_id, configuration, report=report, suppress_errors=suppress_errors)
|
||||
root.addHandler(handler)
|
||||
|
||||
LogRecordId.DEFAULT_PROCESS_ID = str(uuid.uuid4()) # assign default process identifier for log records
|
||||
atexit.register(handler.rotate)
|
||||
|
||||
return handler
|
||||
|
||||
def emit(self, record: logging.LogRecord) -> None:
|
||||
@ -90,8 +99,14 @@ class HttpLogHandler(logging.Handler):
|
||||
return # in case if no package base supplied we need just skip log message
|
||||
|
||||
try:
|
||||
self.reporter.package_logs_add(log_record_id, record.created, record.getMessage())
|
||||
self.reporter.package_logs_add(LogRecord(log_record_id, record.created, record.getMessage()))
|
||||
except Exception:
|
||||
if self.suppress_errors:
|
||||
return
|
||||
self.handleError(record)
|
||||
|
||||
def rotate(self) -> None:
|
||||
"""
|
||||
rotate log records, removing older ones
|
||||
"""
|
||||
self.reporter.logs_rotate(self.keep_last_records)
|
||||
|
@ -74,7 +74,7 @@ class LazyLogging:
|
||||
|
||||
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
|
||||
record = current_factory(*args, **kwargs)
|
||||
record.package_id = LogRecordId(package_base, version or "")
|
||||
record.package_id = LogRecordId(package_base, version or "<unknown>")
|
||||
return record
|
||||
|
||||
logging.setLogRecordFactory(package_record_factory)
|
||||
@ -99,24 +99,3 @@ class LazyLogging:
|
||||
yield
|
||||
finally:
|
||||
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)
|
||||
|
@ -21,6 +21,7 @@ import logging
|
||||
|
||||
from logging.config import fileConfig
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log.http_log_handler import HttpLogHandler
|
||||
@ -38,9 +39,9 @@ class LogLoader:
|
||||
DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device
|
||||
"""
|
||||
|
||||
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s"
|
||||
DEFAULT_LOG_LEVEL = logging.DEBUG
|
||||
DEFAULT_SYSLOG_DEVICE = Path("/") / "dev" / "log"
|
||||
DEFAULT_LOG_FORMAT: ClassVar[str] = "[%(levelname)s %(asctime)s] [%(name)s]: %(message)s"
|
||||
DEFAULT_LOG_LEVEL: ClassVar[int] = logging.DEBUG
|
||||
DEFAULT_SYSLOG_DEVICE: ClassVar[Path] = Path("/") / "dev" / "log"
|
||||
|
||||
@staticmethod
|
||||
def handler(selected: LogHandler | None) -> LogHandler:
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.http import SyncHttpClient
|
||||
from ahriman.core.report.jinja_template import JinjaTemplate
|
||||
@ -39,8 +41,8 @@ class Telegram(Report, JinjaTemplate, SyncHttpClient):
|
||||
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
|
||||
"""
|
||||
|
||||
TELEGRAM_API_URL = "https://api.telegram.org"
|
||||
TELEGRAM_MAX_CONTENT_LENGTH = 4096
|
||||
TELEGRAM_API_URL: ClassVar[str] = "https://api.telegram.org"
|
||||
TELEGRAM_MAX_CONTENT_LENGTH: ClassVar[int] = 4096
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
|
||||
"""
|
||||
|
@ -144,8 +144,7 @@ class UpdateHandler(PackageInfo, Cleaner):
|
||||
branch="master",
|
||||
)
|
||||
|
||||
with self.suppress_logging():
|
||||
Sources.fetch(cache_dir, source)
|
||||
Sources.fetch(cache_dir, source)
|
||||
remote = Package.from_build(cache_dir, self.architecture, None)
|
||||
|
||||
local = packages.get(remote.base)
|
||||
|
@ -27,10 +27,11 @@ from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
class Client:
|
||||
@ -114,6 +115,14 @@ class Client:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
get package changes
|
||||
@ -185,18 +194,16 @@ class Client:
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
created(float): log created timestamp
|
||||
message(str): log message
|
||||
log_record(LogRecord): log record
|
||||
"""
|
||||
# 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, offset: int = 0) -> list[tuple[float, str]]:
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
@ -206,7 +213,7 @@ class Client:
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[tuple[float, str]]: package logs
|
||||
list[LogRecord]: package logs
|
||||
|
||||
Raises:
|
||||
NotImplementedError: not implemented method
|
||||
@ -354,6 +361,16 @@ class Client:
|
||||
return # skip update in case if package is already known
|
||||
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:
|
||||
"""
|
||||
get internal service status
|
||||
|
@ -23,7 +23,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
@ -75,6 +75,15 @@ class LocalClient(Client):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
get package changes
|
||||
@ -134,18 +143,16 @@ class LocalClient(Client):
|
||||
return packages
|
||||
return [(package, status) for package, status in packages if package.base == package_base]
|
||||
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
created(float): log created timestamp
|
||||
message(str): log message
|
||||
log_record(LogRecord): log record
|
||||
"""
|
||||
self.database.logs_insert(log_record_id, created, message, self.repository_id)
|
||||
self.database.logs_insert(log_record, self.repository_id)
|
||||
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
@ -155,7 +162,7 @@ class LocalClient(Client):
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[tuple[float, str]]: package logs
|
||||
list[LogRecord]: package logs
|
||||
"""
|
||||
return self.database.logs_get(package_base, limit, offset, self.repository_id)
|
||||
|
||||
|
@ -28,7 +28,7 @@ from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
@ -53,9 +53,6 @@ class Watcher(LazyLogging):
|
||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||
self.status = BuildStatus()
|
||||
|
||||
# special variables for updating logs
|
||||
self._last_log_record_id = LogRecordId("", "")
|
||||
|
||||
@property
|
||||
def packages(self) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
@ -81,6 +78,8 @@ class Watcher(LazyLogging):
|
||||
for package, status in self.client.package_get(None)
|
||||
}
|
||||
|
||||
logs_rotate: Callable[[int], None]
|
||||
|
||||
package_changes_get: Callable[[str], Changes]
|
||||
|
||||
package_changes_update: Callable[[str, Changes], None]
|
||||
@ -108,22 +107,9 @@ class Watcher(LazyLogging):
|
||||
except KeyError:
|
||||
raise UnknownPackageError(package_base) from None
|
||||
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
"""
|
||||
make new log record into database
|
||||
package_logs_add: Callable[[LogRecord], None]
|
||||
|
||||
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_get: Callable[[str, int, int], list[LogRecord]]
|
||||
|
||||
package_logs_remove: Callable[[str, str | None], None]
|
||||
|
||||
|
@ -29,7 +29,7 @@ from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
@ -210,6 +210,18 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
|
||||
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:
|
||||
"""
|
||||
get package changes
|
||||
@ -294,28 +306,27 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
|
||||
return []
|
||||
|
||||
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
|
||||
def package_logs_add(self, log_record: LogRecord) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record_id(LogRecordId): log record id
|
||||
created(float): log created timestamp
|
||||
message(str): log message
|
||||
log_record(LogRecord): log record
|
||||
"""
|
||||
payload = {
|
||||
"created": created,
|
||||
"message": message,
|
||||
"version": log_record_id.version,
|
||||
"created": log_record.created,
|
||||
"message": log_record.message,
|
||||
"process_id": log_record.log_record_id.process_id,
|
||||
"version": log_record.log_record_id.version,
|
||||
}
|
||||
|
||||
# this is special case, because we would like to do not suppress exception here
|
||||
# in case of exception raised it will be handled by upstream HttpLogHandler
|
||||
# In the other hand, we force to suppress all http logs here to avoid cyclic reporting
|
||||
self.make_request("POST", self._logs_url(log_record_id.package_base),
|
||||
self.make_request("POST", self._logs_url(log_record.log_record_id.package_base),
|
||||
params=self.repository_id.query(), json=payload, suppress_errors=True)
|
||||
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[tuple[float, str]]:
|
||||
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
@ -325,7 +336,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[tuple[float, str]]: package logs
|
||||
list[LogRecord]: package logs
|
||||
"""
|
||||
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
|
||||
|
||||
@ -333,7 +344,7 @@ class WebClient(Client, SyncAhrimanClient):
|
||||
response = self.make_request("GET", self._logs_url(package_base), params=query)
|
||||
response_json = response.json()
|
||||
|
||||
return [(record["created"], record["message"]) for record in response_json]
|
||||
return [LogRecord.from_json(package_base, record) for record in response_json]
|
||||
|
||||
return []
|
||||
|
||||
|
@ -22,6 +22,7 @@ import itertools
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
@ -35,7 +36,7 @@ class PkgbuildGenerator:
|
||||
PKGBUILD_STATIC_PROPERTIES(list[PkgbuildPatch]): (class attribute) list of default pkgbuild static properties
|
||||
"""
|
||||
|
||||
PKGBUILD_STATIC_PROPERTIES = [
|
||||
PKGBUILD_STATIC_PROPERTIES: ClassVar[list[PkgbuildPatch]] = [
|
||||
PkgbuildPatch("pkgrel", "1"),
|
||||
PkgbuildPatch("arch", ["any"]),
|
||||
]
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from collections.abc import Callable
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.configuration.schema import ConfigurationSchema
|
||||
@ -56,8 +57,8 @@ class Trigger(LazyLogging):
|
||||
>>> loader.on_result(Result(), [])
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA: ConfigurationSchema = {}
|
||||
CONFIGURATION_SCHEMA_FALLBACK: str | None = None
|
||||
CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {}
|
||||
CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
|
@ -25,9 +25,9 @@ class Action(StrEnum):
|
||||
base action enumeration
|
||||
|
||||
Attributes:
|
||||
List(Action): (class attribute) list available values
|
||||
Remove(Action): (class attribute) remove everything from local storage
|
||||
Update(Action): (class attribute) update local storage or add to
|
||||
List(Action): list available values
|
||||
Remove(Action): remove everything from local storage
|
||||
Update(Action): update local storage or add to
|
||||
"""
|
||||
|
||||
List = "list"
|
||||
|
@ -27,10 +27,10 @@ class AuthSettings(StrEnum):
|
||||
web authorization type
|
||||
|
||||
Attributes:
|
||||
Disabled(AuthSettings): (class attribute) authorization is disabled
|
||||
Configuration(AuthSettings): (class attribute) configuration based authorization
|
||||
OAuth(AuthSettings): (class attribute) OAuth based provider
|
||||
PAM(AuthSettings): (class attribute) PAM based provider
|
||||
Disabled(AuthSettings): authorization is disabled
|
||||
Configuration(AuthSettings): configuration based authorization
|
||||
OAuth(AuthSettings): OAuth based provider
|
||||
PAM(AuthSettings): PAM based provider
|
||||
"""
|
||||
|
||||
Disabled = "disabled"
|
||||
|
@ -29,11 +29,11 @@ class BuildStatusEnum(StrEnum):
|
||||
build status enumeration
|
||||
|
||||
Attributes:
|
||||
Unknown(BuildStatusEnum): (class attribute) build status is unknown
|
||||
Pending(BuildStatusEnum): (class attribute) package is out-of-dated and will be built soon
|
||||
Building(BuildStatusEnum): (class attribute) package is building right now
|
||||
Failed(BuildStatusEnum): (class attribute) package build failed
|
||||
Success(BuildStatusEnum): (class attribute) package has been built without errors
|
||||
Unknown(BuildStatusEnum): build status is unknown
|
||||
Pending(BuildStatusEnum): package is out-of-dated and will be built soon
|
||||
Building(BuildStatusEnum): package is building right now
|
||||
Failed(BuildStatusEnum): package build failed
|
||||
Success(BuildStatusEnum): package has been built without errors
|
||||
"""
|
||||
|
||||
Unknown = "unknown"
|
||||
|
@ -28,10 +28,10 @@ class EventType(StrEnum):
|
||||
predefined event types
|
||||
|
||||
Attributes:
|
||||
PackageOutdated(EventType): (class attribute) package has been marked as out-of-date
|
||||
PackageRemoved(EventType): (class attribute) package has been removed
|
||||
PackageUpdateFailed(EventType): (class attribute) package update has been failed
|
||||
PackageUpdated(EventType): (class attribute) package has been updated
|
||||
PackageOutdated(EventType): package has been marked as out-of-date
|
||||
PackageRemoved(EventType): package has been removed
|
||||
PackageUpdateFailed(EventType): package update has been failed
|
||||
PackageUpdated(EventType): package has been updated
|
||||
"""
|
||||
|
||||
PackageOutdated = "package-outdated"
|
||||
@ -78,7 +78,7 @@ class Event:
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: dependencies object
|
||||
Self: event object
|
||||
"""
|
||||
return cls(
|
||||
event=dump["event"],
|
||||
|
@ -23,6 +23,7 @@ from typing import Any, Self
|
||||
from ahriman.core.utils import dataclass_view
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@ -35,6 +36,7 @@ class InternalStatus:
|
||||
architecture(str | None): repository architecture
|
||||
packages(Counters): packages statuses counter object
|
||||
repository(str | None): repository name
|
||||
stats(RepositoryStats | None): repository stats
|
||||
version(str | None): service version
|
||||
"""
|
||||
|
||||
@ -42,6 +44,7 @@ class InternalStatus:
|
||||
architecture: str | None = None
|
||||
packages: Counters = field(default=Counters(total=0))
|
||||
repository: str | None = None
|
||||
stats: RepositoryStats | None = None
|
||||
version: str | None = None
|
||||
|
||||
@classmethod
|
||||
@ -56,11 +59,13 @@ class InternalStatus:
|
||||
Self: internal status
|
||||
"""
|
||||
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 {}
|
||||
return cls(status=BuildStatus.from_json(build_status),
|
||||
architecture=dump.get("architecture"),
|
||||
packages=counters,
|
||||
repository=dump.get("repository"),
|
||||
stats=stats,
|
||||
version=dump.get("version"))
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
|
@ -25,9 +25,9 @@ class LogHandler(StrEnum):
|
||||
log handler as described by default configuration
|
||||
|
||||
Attributes:
|
||||
Console(LogHandler): (class attribute) write logs to console
|
||||
Syslog(LogHandler): (class attribute) write logs to syslog device /dev/null
|
||||
Journald(LogHandler): (class attribute) write logs to journald directly
|
||||
Console(LogHandler): write logs to console
|
||||
Syslog(LogHandler): write logs to syslog device /dev/null
|
||||
Journald(LogHandler): write logs to journald directly
|
||||
"""
|
||||
|
||||
Console = "console"
|
||||
|
76
src/ahriman/models/log_record.py
Normal file
76
src/ahriman/models/log_record.py
Normal file
@ -0,0 +1,76 @@
|
||||
#
|
||||
# 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
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogRecord:
|
||||
"""
|
||||
log record
|
||||
|
||||
Attributes:
|
||||
log_record_id(LogRecordId): log record identifier
|
||||
created(float): log record creation timestamp
|
||||
message(str): log record message
|
||||
"""
|
||||
|
||||
log_record_id: LogRecordId
|
||||
created: float
|
||||
message: str
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, package_base: str, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct log record from the json dump
|
||||
|
||||
Args:
|
||||
package_base(str): package base for which log record belongs
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: log record object
|
||||
"""
|
||||
if "process_id" in dump:
|
||||
log_record_id = LogRecordId(package_base, dump["version"], dump["process_id"])
|
||||
else:
|
||||
log_record_id = LogRecordId(package_base, dump["version"])
|
||||
|
||||
return cls(
|
||||
log_record_id=log_record_id,
|
||||
created=dump["created"],
|
||||
message=dump["message"],
|
||||
)
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate json log record view
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: json-friendly dictionary
|
||||
"""
|
||||
return {
|
||||
"created": self.created,
|
||||
"message": self.message,
|
||||
"version": self.log_record_id.version,
|
||||
"process_id": self.log_record_id.process_id,
|
||||
}
|
@ -17,7 +17,10 @@
|
||||
# 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 uuid
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@ -26,9 +29,21 @@ class LogRecordId:
|
||||
log record process identifier
|
||||
|
||||
Attributes:
|
||||
DEFAULT_PROCESS_ID(str): (class attribute) default process identifier
|
||||
package_base(str): package base for which log record belongs
|
||||
version(str): package version for which log record belongs
|
||||
process_id(str, optional): unique process identifier
|
||||
"""
|
||||
|
||||
package_base: str
|
||||
version: str
|
||||
process_id: str = ""
|
||||
|
||||
DEFAULT_PROCESS_ID: ClassVar[str] = str(uuid.uuid4())
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""
|
||||
assign process identifier from default if not set
|
||||
"""
|
||||
if not self.process_id:
|
||||
object.__setattr__(self, "process_id", self.DEFAULT_PROCESS_ID)
|
||||
|
@ -429,14 +429,11 @@ class Package(LazyLogging):
|
||||
task = Task(self, configuration, repository_id.architecture, paths)
|
||||
|
||||
try:
|
||||
with self.suppress_logging():
|
||||
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
||||
task.init(paths.cache_for(self.base), [], None)
|
||||
task.build(paths.cache_for(self.base), dry_run=True)
|
||||
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
||||
task.init(paths.cache_for(self.base), [], None)
|
||||
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.base) / "PKGBUILD")
|
||||
|
||||
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:
|
||||
self.logger.exception("cannot determine version of VCS package")
|
||||
finally:
|
||||
|
@ -32,13 +32,13 @@ class PackageSource(StrEnum):
|
||||
package source for addition enumeration
|
||||
|
||||
Attributes:
|
||||
Auto(PackageSource): (class attribute) automatically determine type of the source
|
||||
Archive(PackageSource): (class attribute) source is a package archive
|
||||
AUR(PackageSource): (class attribute) source is an AUR package for which it should search
|
||||
Directory(PackageSource): (class attribute) source is a directory which contains packages
|
||||
Local(PackageSource): (class attribute) source is locally stored PKGBUILD
|
||||
Remote(PackageSource): (class attribute) source is remote (http, ftp etc...) link
|
||||
Repository(PackageSource): (class attribute) source is official repository
|
||||
Auto(PackageSource): automatically determine type of the source
|
||||
Archive(PackageSource): source is a package archive
|
||||
AUR(PackageSource): source is an AUR package for which it should search
|
||||
Directory(PackageSource): source is a directory which contains packages
|
||||
Local(PackageSource): source is locally stored PKGBUILD
|
||||
Remote(PackageSource): source is remote (http, ftp etc...) link
|
||||
Repository(PackageSource): source is official repository
|
||||
|
||||
Examples:
|
||||
In case if source is unknown the :func:`resolve()` and the source
|
||||
|
@ -25,9 +25,9 @@ class PacmanSynchronization(IntEnum):
|
||||
pacman database synchronization flag
|
||||
|
||||
Attributes:
|
||||
Disabled(PacmanSynchronization): (class attribute) do not synchronize local database
|
||||
Enabled(PacmanSynchronization): (class attribute) synchronize local database (same as pacman -Sy)
|
||||
Force(PacmanSynchronization): (class attribute) force synchronize local database (same as pacman -Syy)
|
||||
Disabled(PacmanSynchronization): do not synchronize local database
|
||||
Enabled(PacmanSynchronization): synchronize local database (same as pacman -Sy)
|
||||
Force(PacmanSynchronization): force synchronize local database (same as pacman -Syy)
|
||||
"""
|
||||
|
||||
Disabled = 0
|
||||
|
@ -21,9 +21,10 @@ from collections.abc import Iterator, Mapping
|
||||
from dataclasses import dataclass
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any, IO, Self
|
||||
from typing import Any, ClassVar, IO, Self
|
||||
|
||||
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
|
||||
from ahriman.core.exceptions import EncodeError
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
@ -33,11 +34,14 @@ class Pkgbuild(Mapping[str, Any]):
|
||||
model and proxy for PKGBUILD properties
|
||||
|
||||
Attributes:
|
||||
DEFAULT_ENCODINGS(list[str]): (class attribute) list of encoding to be applied on the file content
|
||||
fields(dict[str, PkgbuildPatch]): PKGBUILD fields
|
||||
"""
|
||||
|
||||
fields: dict[str, PkgbuildPatch]
|
||||
|
||||
DEFAULT_ENCODINGS: ClassVar[list[str]] = ["utf8", "latin-1"]
|
||||
|
||||
@property
|
||||
def variables(self) -> dict[str, str]:
|
||||
"""
|
||||
@ -54,18 +58,34 @@ class Pkgbuild(Mapping[str, Any]):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> Self:
|
||||
def from_file(cls, path: Path, encodings: list[str] | None = None) -> Self:
|
||||
"""
|
||||
parse PKGBUILD from the file
|
||||
|
||||
Args:
|
||||
path(Path): path to the PKGBUILD file
|
||||
encodings(list[str] | None, optional): the encoding of the file (Default value = None)
|
||||
|
||||
Returns:
|
||||
Self: constructed instance of self
|
||||
|
||||
Raises:
|
||||
EncodeError: if encoding is unknown
|
||||
"""
|
||||
with path.open(encoding="utf8") as input_file:
|
||||
return cls.from_io(input_file)
|
||||
# read raw bytes from file
|
||||
with path.open("rb") as input_file:
|
||||
content = input_file.read()
|
||||
|
||||
# decode bytes content based on either
|
||||
encodings = encodings or cls.DEFAULT_ENCODINGS
|
||||
for encoding in encodings:
|
||||
try:
|
||||
io = StringIO(content.decode(encoding))
|
||||
return cls.from_io(io)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise EncodeError(encodings)
|
||||
|
||||
@classmethod
|
||||
def from_io(cls, stream: IO[str]) -> Self:
|
||||
|
@ -27,13 +27,13 @@ class ReportSettings(StrEnum):
|
||||
report targets enumeration
|
||||
|
||||
Attributes:
|
||||
Disabled(ReportSettings): (class attribute) option which generates no report for testing purpose
|
||||
HTML(ReportSettings): (class attribute) html report generation
|
||||
Email(ReportSettings): (class attribute) email report generation
|
||||
Console(ReportSettings): (class attribute) print result to console
|
||||
Telegram(ReportSettings): (class attribute) markdown report to telegram channel
|
||||
RSS(ReportSettings): (class attribute) RSS report generation
|
||||
RemoteCall(ReportSettings): (class attribute) remote ahriman server call
|
||||
Disabled(ReportSettings): option which generates no report for testing purpose
|
||||
HTML(ReportSettings): html report generation
|
||||
Email(ReportSettings): email report generation
|
||||
Console(ReportSettings): print result to console
|
||||
Telegram(ReportSettings): markdown report to telegram channel
|
||||
RSS(ReportSettings): RSS report generation
|
||||
RemoteCall(ReportSettings): remote ahriman server call
|
||||
"""
|
||||
|
||||
Disabled = "disabled" # for testing purpose
|
||||
|
@ -97,3 +97,12 @@ class RepositoryId:
|
||||
raise ValueError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'")
|
||||
|
||||
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})"
|
||||
|
77
src/ahriman/models/repository_stats.py
Normal file
77
src/ahriman/models/repository_stats.py
Normal file
@ -0,0 +1,77 @@
|
||||
#
|
||||
# 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()
|
||||
),
|
||||
)
|
@ -20,7 +20,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Self
|
||||
from typing import Any, ClassVar, Self
|
||||
|
||||
from ahriman.models.package import Package
|
||||
|
||||
@ -33,7 +33,7 @@ class Result:
|
||||
STATUS_PRIORITIES(list[str]): (class attribute) list of statues according to their priorities
|
||||
"""
|
||||
|
||||
STATUS_PRIORITIES = [
|
||||
STATUS_PRIORITIES: ClassVar[list[str]] = [
|
||||
"failed",
|
||||
"removed",
|
||||
"updated",
|
||||
|
104
src/ahriman/models/series_statistics.py
Normal file
104
src/ahriman/models/series_statistics.py
Normal file
@ -0,0 +1,104 @@
|
||||
#
|
||||
# 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)
|
@ -27,9 +27,9 @@ class SignSettings(StrEnum):
|
||||
sign targets enumeration
|
||||
|
||||
Attributes:
|
||||
Disabled(SignSettings): (class attribute) option which generates no report for testing purpose
|
||||
Packages(SignSettings): (class attribute) sign each package
|
||||
Repository(SignSettings): (class attribute) sign repository database file
|
||||
Disabled(SignSettings): option which generates no report for testing purpose
|
||||
Packages(SignSettings): sign each package
|
||||
Repository(SignSettings): sign repository database file
|
||||
"""
|
||||
|
||||
Disabled = "disabled"
|
||||
|
@ -27,9 +27,9 @@ class SmtpSSLSettings(StrEnum):
|
||||
SMTP SSL mode enumeration
|
||||
|
||||
Attributes:
|
||||
Disabled(SmtpSSLSettings): (class attribute) no SSL enabled
|
||||
SSL(SmtpSSLSettings): (class attribute) use SMTP_SSL instead of normal SMTP client
|
||||
STARTTLS(SmtpSSLSettings): (class attribute) use STARTTLS in normal SMTP client
|
||||
Disabled(SmtpSSLSettings): no SSL enabled
|
||||
SSL(SmtpSSLSettings): use SMTP_SSL instead of normal SMTP client
|
||||
STARTTLS(SmtpSSLSettings): use STARTTLS in normal SMTP client
|
||||
"""
|
||||
|
||||
Disabled = "disabled"
|
||||
|
@ -27,11 +27,11 @@ class UploadSettings(StrEnum):
|
||||
remote synchronization targets enumeration
|
||||
|
||||
Attributes:
|
||||
Disabled(UploadSettings): (class attribute) no sync will be performed, required for testing purpose
|
||||
Rsync(UploadSettings): (class attribute) sync via rsync
|
||||
S3(UploadSettings): (class attribute) sync to Amazon S3
|
||||
GitHub(UploadSettings): (class attribute) sync to GitHub releases page
|
||||
RemoteService(UploadSettings): (class attribute) sync to another ahriman instance
|
||||
Disabled(UploadSettings): no sync will be performed, required for testing purpose
|
||||
Rsync(UploadSettings): sync via rsync
|
||||
S3(UploadSettings): sync to Amazon S3
|
||||
GitHub(UploadSettings): sync to GitHub releases page
|
||||
RemoteService(UploadSettings): sync to another ahriman instance
|
||||
"""
|
||||
|
||||
Disabled = "disabled" # for testing purpose
|
||||
|
@ -21,7 +21,7 @@ import bcrypt
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from secrets import token_urlsafe as generate_password
|
||||
from typing import Self
|
||||
from typing import ClassVar, Self
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
|
||||
@ -69,7 +69,7 @@ class User:
|
||||
packager_id: str | None = None
|
||||
key: str | None = None
|
||||
|
||||
SUPPORTED_ALGOS = {"$2$", "$2a$", "$2x$", "$2y$", "$2b$"}
|
||||
SUPPORTED_ALGOS: ClassVar[set[str]] = {"$2$", "$2a$", "$2x$", "$2y$", "$2b$"}
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""
|
||||
|
@ -27,11 +27,11 @@ class UserAccess(StrEnum):
|
||||
web user access enumeration
|
||||
|
||||
Attributes:
|
||||
Unauthorized(UserAccess): (class attribute) user can access specific resources which are marked as available
|
||||
Unauthorized(UserAccess): user can access specific resources which are marked as available
|
||||
without authorization (e.g. login, logout, static)
|
||||
Read(UserAccess): (class attribute) user can read the page
|
||||
Reporter(UserAccess): (class attribute) user can read everything and is able to perform some modifications
|
||||
Full(UserAccess): (class attribute) user has full access
|
||||
Read(UserAccess): user can read the page
|
||||
Reporter(UserAccess): user can read everything and is able to perform some modifications
|
||||
Full(UserAccess): user has full access
|
||||
"""
|
||||
|
||||
Unauthorized = "unauthorized"
|
||||
|
@ -18,7 +18,8 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPException
|
||||
from typing import Any, Callable
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.apispec import Schema, aiohttp_apispec
|
||||
|
@ -31,6 +31,7 @@ from ahriman.web.schemas.info_schema import InfoSchema
|
||||
from ahriman.web.schemas.internal_status_schema import InternalStatusSchema
|
||||
from ahriman.web.schemas.log_schema import LogSchema
|
||||
from ahriman.web.schemas.login_schema import LoginSchema
|
||||
from ahriman.web.schemas.logs_rotate_schema import LogsRotateSchema
|
||||
from ahriman.web.schemas.logs_schema import LogsSchema
|
||||
from ahriman.web.schemas.oauth2_schema import OAuth2Schema
|
||||
from ahriman.web.schemas.package_name_schema import PackageNameSchema
|
||||
@ -49,8 +50,8 @@ from ahriman.web.schemas.process_id_schema import ProcessIdSchema
|
||||
from ahriman.web.schemas.process_schema import ProcessSchema
|
||||
from ahriman.web.schemas.remote_schema import RemoteSchema
|
||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||
from ahriman.web.schemas.repository_stats_schema import RepositoryStatsSchema
|
||||
from ahriman.web.schemas.search_schema import SearchSchema
|
||||
from ahriman.web.schemas.status_schema import StatusSchema
|
||||
from ahriman.web.schemas.update_flags_schema import UpdateFlagsSchema
|
||||
from ahriman.web.schemas.versioned_log_schema import VersionedLogSchema
|
||||
from ahriman.web.schemas.worker_schema import WorkerSchema
|
||||
|
@ -25,6 +25,6 @@ class FileSchema(Schema):
|
||||
request file upload schema
|
||||
"""
|
||||
|
||||
archive = fields.Field(required=True, metadata={
|
||||
archive = fields.Raw(required=True, metadata={
|
||||
"description": "Package archive to be uploaded",
|
||||
})
|
||||
|
@ -21,6 +21,7 @@ from ahriman import __version__
|
||||
from ahriman.web.apispec import fields
|
||||
from ahriman.web.schemas.counters_schema import CountersSchema
|
||||
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
|
||||
|
||||
|
||||
@ -32,6 +33,9 @@ class InternalStatusSchema(RepositoryIdSchema):
|
||||
packages = fields.Nested(CountersSchema(), required=True, metadata={
|
||||
"description": "Repository package counters",
|
||||
})
|
||||
stats = fields.Nested(RepositoryStatsSchema(), required=True, metadata={
|
||||
"description": "Repository stats",
|
||||
})
|
||||
status = fields.Nested(StatusSchema(), required=True, metadata={
|
||||
"description": "Repository status as stored by web service",
|
||||
})
|
||||
|
@ -17,12 +17,13 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman import __version__
|
||||
from ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class LogSchema(Schema):
|
||||
"""
|
||||
request package log schema
|
||||
request and response package log schema
|
||||
"""
|
||||
|
||||
created = fields.Float(required=True, metadata={
|
||||
@ -32,3 +33,10 @@ class LogSchema(Schema):
|
||||
message = fields.String(required=True, metadata={
|
||||
"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",
|
||||
})
|
||||
|
@ -17,18 +17,14 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman import __version__
|
||||
from ahriman.web.apispec import fields
|
||||
from ahriman.web.schemas.log_schema import LogSchema
|
||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||
from ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class VersionedLogSchema(LogSchema, RepositoryIdSchema):
|
||||
class LogsRotateSchema(Schema):
|
||||
"""
|
||||
request package log schema
|
||||
request logs rotate schema
|
||||
"""
|
||||
|
||||
version = fields.Integer(required=True, metadata={
|
||||
"description": "Package version to tag",
|
||||
"example": __version__,
|
||||
keep_last_records = fields.Integer(metadata={
|
||||
"description": "Keep the specified amount of records",
|
||||
})
|
43
src/ahriman/web/schemas/repository_stats_schema.py
Normal file
43
src/ahriman/web/schemas/repository_stats_schema.py
Normal file
@ -0,0 +1,43 @@
|
||||
#
|
||||
# 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,
|
||||
})
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
import aiohttp_jinja2
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -35,7 +35,7 @@ class DocsView(BaseView):
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
|
||||
ROUTES = ["/api-docs"]
|
||||
|
||||
@classmethod
|
||||
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
from aiohttp.web import Response, json_response
|
||||
from collections.abc import Callable
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import partition
|
||||
@ -35,7 +36,7 @@ class SwaggerView(BaseView):
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
|
||||
ROUTES = ["/api-docs/swagger.json"]
|
||||
|
||||
@classmethod
|
||||
|
@ -20,7 +20,7 @@
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
|
||||
from aiohttp_cors import CorsViewMixin # type: ignore[import-untyped]
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import TypeVar
|
||||
from typing import ClassVar, TypeVar
|
||||
|
||||
from ahriman.core.auth import Auth
|
||||
from ahriman.core.configuration import Configuration
|
||||
@ -46,8 +46,8 @@ class BaseView(View, CorsViewMixin):
|
||||
ROUTES(list[str]): (class attribute) list of supported routes
|
||||
"""
|
||||
|
||||
OPTIONS_PERMISSION = UserAccess.Unauthorized
|
||||
ROUTES: list[str] = []
|
||||
OPTIONS_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
|
||||
ROUTES: ClassVar[list[str]] = []
|
||||
|
||||
@property
|
||||
def configuration(self) -> Configuration:
|
||||
|
@ -19,7 +19,7 @@
|
||||
#
|
||||
import aiohttp_jinja2
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from ahriman.core.auth.helpers import authorized_userid
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -48,7 +48,7 @@ class IndexView(BaseView):
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
|
||||
ROUTES = ["/", "/index.html"]
|
||||
|
||||
@aiohttp_jinja2.template("build-status.jinja2")
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPFound, HTTPNotFound
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.views.base import BaseView
|
||||
@ -31,7 +32,7 @@ class StaticView(BaseView):
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Unauthorized
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
|
||||
ROUTES = ["/favicon.ico"]
|
||||
|
||||
async def get(self) -> None:
|
||||
|
@ -17,6 +17,8 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
@ -25,7 +27,7 @@ class StatusViewGuard:
|
||||
helper for check if status routes are enabled
|
||||
"""
|
||||
|
||||
ROUTES: list[str]
|
||||
ROUTES: ClassVar[list[str]]
|
||||
|
||||
@classmethod
|
||||
def routes(cls, configuration: Configuration) -> list[str]:
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.event import Event
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -35,7 +36,7 @@ class EventsView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
|
||||
ROUTES = ["/api/v1/events"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -19,6 +19,7 @@
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
from collections.abc import Callable
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.models.worker import Worker
|
||||
@ -37,7 +38,7 @@ class WorkersView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
|
||||
ROUTES = ["/api/v1/distributed"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -36,8 +37,8 @@ class ChangesView(StatusViewGuard, BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
|
||||
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages/{package}/changes"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -36,8 +37,8 @@ class DependenciesView(StatusViewGuard, BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
|
||||
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages/{package}/dependencies"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -18,14 +18,14 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.utils import pretty_datetime
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.apispec.decorators import apidocs
|
||||
from ahriman.web.schemas import LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema, \
|
||||
VersionedLogSchema
|
||||
from ahriman.web.schemas import LogSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema
|
||||
from ahriman.web.views.base import BaseView
|
||||
from ahriman.web.views.status_view_guard import StatusViewGuard
|
||||
|
||||
@ -40,8 +40,8 @@ class LogsView(StatusViewGuard, BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
|
||||
ROUTES = ["/api/v1/packages/{package}/logs"]
|
||||
|
||||
@apidocs(
|
||||
@ -97,7 +97,7 @@ class LogsView(StatusViewGuard, BaseView):
|
||||
response = {
|
||||
"package_base": package_base,
|
||||
"status": status.view(),
|
||||
"logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for created, message in logs)
|
||||
"logs": "\n".join(f"[{pretty_datetime(log_record.created)}] {log_record.message}" for log_record in logs)
|
||||
}
|
||||
return json_response(response)
|
||||
|
||||
@ -109,7 +109,7 @@ class LogsView(StatusViewGuard, BaseView):
|
||||
error_400_enabled=True,
|
||||
error_404_description="Repository is unknown",
|
||||
match_schema=PackageNameSchema,
|
||||
body_schema=VersionedLogSchema,
|
||||
body_schema=LogSchema,
|
||||
)
|
||||
async def post(self) -> None:
|
||||
"""
|
||||
@ -123,12 +123,10 @@ class LogsView(StatusViewGuard, BaseView):
|
||||
|
||||
try:
|
||||
data = await self.request.json()
|
||||
created = data["created"]
|
||||
record = data["message"]
|
||||
version = data["version"]
|
||||
log_record = LogRecord.from_json(package_base, data)
|
||||
except Exception as ex:
|
||||
raise HTTPBadRequest(reason=str(ex))
|
||||
|
||||
self.service().package_logs_add(LogRecordId(package_base, version), created, record)
|
||||
self.service().package_logs_add(log_record)
|
||||
|
||||
raise HTTPNoContent
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
@ -40,8 +41,8 @@ class PackageView(StatusViewGuard, BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = UserAccess.Read
|
||||
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
|
||||
ROUTES = ["/api/v1/packages/{package}"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -21,6 +21,7 @@ import itertools
|
||||
|
||||
from aiohttp.web import HTTPNoContent, Response, json_response
|
||||
from collections.abc import Callable
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.build_status import BuildStatus
|
||||
from ahriman.models.package import Package
|
||||
@ -40,8 +41,8 @@ class PackagesView(StatusViewGuard, BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Read
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
|
||||
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.apispec.decorators import apidocs
|
||||
@ -35,8 +36,8 @@ class PatchView(StatusViewGuard, BaseView):
|
||||
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
|
||||
"""
|
||||
|
||||
DELETE_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
DELETE_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
|
||||
ROUTES = ["/api/v1/packages/{package}/patches/{patch}"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -36,8 +37,8 @@ class PatchesView(StatusViewGuard, BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
GET_PERMISSION = UserAccess.Reporter
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
|
||||
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||
ROUTES = ["/api/v1/packages/{package}/patches"]
|
||||
|
||||
@apidocs(
|
||||
|
@ -18,6 +18,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from aiohttp.web import HTTPBadRequest, Response, json_response
|
||||
from typing import ClassVar
|
||||
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.user_access import UserAccess
|
||||
@ -34,7 +35,7 @@ class AddView(BaseView):
|
||||
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
|
||||
"""
|
||||
|
||||
POST_PERMISSION = UserAccess.Full
|
||||
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
|
||||
ROUTES = ["/api/v1/service/add"]
|
||||
|
||||
@apidocs(
|
||||
|
64
src/ahriman/web/views/v1/service/logs.py
Normal file
64
src/ahriman/web/views/v1/service/logs.py
Normal file
@ -0,0 +1,64 @@
|
||||
#
|
||||
# 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 typing import ClassVar
|
||||
|
||||
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: ClassVar[UserAccess] = 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
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user