mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-09-11 03:09:56 +00:00
Compare commits
1 Commits
08640d9108
...
2.17.2
Author | SHA1 | Date | |
---|---|---|---|
e0f1563f62 |
1067
docs/_static/architecture.dot
vendored
1067
docs/_static/architecture.dot
vendored
File diff suppressed because it is too large
Load Diff
@ -92,14 +92,6 @@ ahriman.core.formatters.repository\_printer module
|
||||
:no-undoc-members:
|
||||
: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
|
||||
----------------------------------------------
|
||||
|
||||
|
@ -236,14 +236,6 @@ 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
|
||||
----------------------------
|
||||
|
||||
@ -260,14 +252,6 @@ 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
|
||||
------------------------------------
|
||||
|
||||
|
@ -260,14 +260,6 @@ 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
|
||||
-----------------------------------------
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
pkgbase='ahriman'
|
||||
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
|
||||
pkgver=2.17.1
|
||||
pkgver=2.17.2
|
||||
pkgrel=1
|
||||
pkgdesc="ArcH linux ReposItory MANager"
|
||||
arch=('any')
|
||||
|
@ -36,9 +36,7 @@
|
||||
|
||||
<div class="container">
|
||||
<div id="toolbar" class="dropdown">
|
||||
<button id="dashboard-button" type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#dashboard-modal">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</button>
|
||||
<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>
|
||||
|
||||
{% 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">
|
||||
@ -154,7 +152,6 @@
|
||||
|
||||
{% 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" %}
|
||||
|
@ -1,76 +0,0 @@
|
||||
<div id="dashboard-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div id="dashboard-modal-header" class="modal-header">
|
||||
<h4 class="modal-title">System health</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Repository name</div>
|
||||
<div id="dashboard-name" class="col-8 col-lg-3"></div>
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Repository architecture</div>
|
||||
<div id="dashboard-architecture" class="col-8 col-lg-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Current status</div>
|
||||
<div id="dashboard-status" class="col-8 col-lg-3"></div>
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Updated at</div>
|
||||
<div id="dashboard-status-timestamp" class="col-8 col-lg-3"></div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-canvas" class="form-group row mt-2">
|
||||
<div class="col-8 col-lg-6">
|
||||
<canvas id="dashboard-packages-count-chart"></canvas>
|
||||
</div>
|
||||
<div class="col-8 col-lg-6">
|
||||
<canvas id="dashboard-packages-statuses-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dashboardModal = document.getElementById("dashboard-modal");
|
||||
const dashboardModalHeader = document.getElementById("dashboard-modal-header");
|
||||
|
||||
const dashboardName = document.getElementById("dashboard-name");
|
||||
const dashboardArchitecture = document.getElementById("dashboard-architecture");
|
||||
const dashboardStatus = document.getElementById("dashboard-status");
|
||||
const dashboardStatusTimestamp = document.getElementById("dashboard-status-timestamp");
|
||||
|
||||
const dashboardCanvas = document.getElementById("dashboard-canvas");
|
||||
const dashboardPackagesStatusesChartCanvas = document.getElementById("dashboard-packages-statuses-chart");
|
||||
let dashboardPackagesStatusesChart = null;
|
||||
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
|
||||
let dashboardPackagesCountChart = null;
|
||||
|
||||
ready(_ => {
|
||||
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
|
||||
type: "pie",
|
||||
data: {},
|
||||
options: {
|
||||
responsive: true,
|
||||
},
|
||||
});
|
||||
dashboardPackagesCountChart = new Chart(dashboardPackagesCountChartCanvas, {
|
||||
type: "bar",
|
||||
data: {},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
@ -296,6 +296,14 @@
|
||||
}
|
||||
|
||||
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 dashboardButton = document.getElementById("dashboard-button");
|
||||
const statusBadge = document.getElementById("badge-status");
|
||||
const versionBadge = document.getElementById("badge-version");
|
||||
|
||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||
@ -141,62 +141,14 @@
|
||||
data => {
|
||||
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
||||
|
||||
dashboardButton.classList.remove(...dashboardButton.classList);
|
||||
dashboardButton.classList.add("btn");
|
||||
dashboardButton.classList.add(badgeClass(data.status.status));
|
||||
statusBadge.classList.remove(...statusBadge.classList);
|
||||
statusBadge.classList.add("btn");
|
||||
statusBadge.classList.add(badgeClass(data.status.status));
|
||||
|
||||
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
|
||||
dashboardModalHeader.classList.add("modal-header");
|
||||
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
|
||||
|
||||
dashboardName.textContent = data.repository;
|
||||
dashboardArchitecture.textContent = data.architecture;
|
||||
dashboardStatus.textContent = data.status.status;
|
||||
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
|
||||
|
||||
if (dashboardPackagesStatusesChart) {
|
||||
const labels = [
|
||||
"unknown",
|
||||
"pending",
|
||||
"building",
|
||||
"failed",
|
||||
"success",
|
||||
];
|
||||
dashboardPackagesStatusesChart.config.data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: "packages in status",
|
||||
data: labels.map(label => data.packages[label]),
|
||||
backgroundColor: [
|
||||
"rgb(55, 58, 60)",
|
||||
"rgb(255, 117, 24)",
|
||||
"rgb(255, 117, 24)",
|
||||
"rgb(255, 0, 57)",
|
||||
"rgb(63, 182, 24)", // copy-paste from current style
|
||||
],
|
||||
}],
|
||||
};
|
||||
dashboardPackagesStatusesChart.update();
|
||||
}
|
||||
|
||||
if (dashboardPackagesCountChart) {
|
||||
dashboardPackagesCountChart.config.data = {
|
||||
labels: ["packages"],
|
||||
datasets: [
|
||||
{
|
||||
label: "archives",
|
||||
data: [data.stats.packages],
|
||||
},
|
||||
{
|
||||
label: "bases",
|
||||
data: [data.stats.bases],
|
||||
},
|
||||
],
|
||||
};
|
||||
dashboardPackagesCountChart.update();
|
||||
}
|
||||
|
||||
dashboardCanvas.hidden = data.status.total > 0;
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -275,6 +227,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
bootstrap.Popover.getOrCreateInstance(statusBadge);
|
||||
selectRepository();
|
||||
});
|
||||
</script>
|
||||
|
@ -58,14 +58,6 @@
|
||||
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()
|
||||
|
@ -1,4 +1,4 @@
|
||||
.TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual"
|
||||
.TH AHRIMAN "1" "2025\-02\-23" "ahriman" "Generated Python Manual"
|
||||
.SH NAME
|
||||
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.1"
|
||||
__version__ = "2.17.2"
|
||||
|
@ -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, RepositoryStatsPrinter
|
||||
from ahriman.core.formatters import EventStatsPrinter, PackageStatsPrinter
|
||||
from ahriman.core.utils import enum_values, pretty_datetime
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
@ -64,7 +64,6 @@ 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)
|
||||
|
@ -28,7 +28,6 @@ from ahriman.core.formatters.package_stats_printer import PackageStatsPrinter
|
||||
from ahriman.core.formatters.patch_printer import PatchPrinter
|
||||
from ahriman.core.formatters.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,9 +17,11 @@
|
||||
# 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):
|
||||
@ -27,7 +29,7 @@ class EventStatsPrinter(StringPrinter):
|
||||
print event statistics
|
||||
|
||||
Attributes:
|
||||
statistics(SeriesStatistics): statistics object
|
||||
events(list[float | int]): event values to build statistics
|
||||
"""
|
||||
|
||||
def __init__(self, event_type: str, events: list[float | int]) -> None:
|
||||
@ -37,7 +39,7 @@ class EventStatsPrinter(StringPrinter):
|
||||
events(list[float | int]): event values to build statistics
|
||||
"""
|
||||
StringPrinter.__init__(self, event_type)
|
||||
self.statistics = SeriesStatistics(events)
|
||||
self.events = events
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
"""
|
||||
@ -47,17 +49,24 @@ class EventStatsPrinter(StringPrinter):
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
properties = [
|
||||
Property("total", self.statistics.total),
|
||||
Property("total", len(self.events)),
|
||||
]
|
||||
|
||||
# time statistics
|
||||
if self.statistics:
|
||||
mean = self.statistics.mean
|
||||
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}"
|
||||
|
||||
properties.extend([
|
||||
Property("min", self.statistics.min),
|
||||
Property("average", f"{mean:.3f} ± {self.statistics.st_dev:.3f}"),
|
||||
Property("max", self.statistics.max),
|
||||
Property("min", min_time),
|
||||
Property("average", average),
|
||||
Property("max", max_time),
|
||||
])
|
||||
|
||||
return properties
|
||||
|
@ -1,53 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.core.utils import pretty_size
|
||||
from ahriman.models.property import Property
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
class RepositoryStatsPrinter(StringPrinter):
|
||||
"""
|
||||
print repository statistics
|
||||
|
||||
Attributes:
|
||||
statistics(RepositoryStats): repository statistics
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, statistics: RepositoryStats) -> None:
|
||||
"""
|
||||
Args:
|
||||
statistics(RepositoryStats): repository statistics
|
||||
"""
|
||||
StringPrinter.__init__(self, str(repository_id))
|
||||
self.statistics = statistics
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
|
||||
Returns:
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
return [
|
||||
Property("Packages", self.statistics.bases),
|
||||
Property("Repository size", pretty_size(self.statistics.archive_size)),
|
||||
]
|
@ -31,7 +31,6 @@ from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
class Client:
|
||||
@ -355,16 +354,6 @@ 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,6 @@ 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)
|
||||
@ -36,7 +35,6 @@ 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
|
||||
"""
|
||||
|
||||
@ -44,7 +42,6 @@ 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
|
||||
@ -59,13 +56,11 @@ 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]:
|
||||
|
@ -97,12 +97,3 @@ 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})"
|
||||
|
@ -1,77 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.utils import filter_json
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RepositoryStats:
|
||||
"""
|
||||
repository stats representation
|
||||
"""
|
||||
|
||||
bases: int
|
||||
packages: int
|
||||
archive_size: int
|
||||
installed_size: int
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct counters from json dump
|
||||
|
||||
Args:
|
||||
dump(dict[str, Any]): json dump body
|
||||
|
||||
Returns:
|
||||
Self: status counters
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
@classmethod
|
||||
def from_packages(cls, packages: list[Package]) -> Self:
|
||||
"""
|
||||
construct statistics from list of repository packages
|
||||
|
||||
Args:
|
||||
packages(list[Packages]): list of repository packages
|
||||
|
||||
Returns:
|
||||
Self: constructed statistics object
|
||||
"""
|
||||
return cls(
|
||||
bases=len(packages),
|
||||
packages=sum(len(package.packages) for package in packages),
|
||||
archive_size=sum(
|
||||
archive.archive_size or 0
|
||||
for package in packages
|
||||
for archive in package.packages.values()
|
||||
),
|
||||
installed_size=sum(
|
||||
archive.installed_size or 0
|
||||
for package in packages
|
||||
for archive in package.packages.values()
|
||||
),
|
||||
)
|
@ -1,104 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
import statistics
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SeriesStatistics:
|
||||
"""
|
||||
series statistics helper
|
||||
|
||||
Attributes:
|
||||
series(list[float | int]): list of values to be processed
|
||||
"""
|
||||
|
||||
series: list[float | int]
|
||||
|
||||
@property
|
||||
def max(self) -> float | int | None:
|
||||
"""
|
||||
get max value in series
|
||||
|
||||
Returns:
|
||||
float | int | None: ``None`` if series is empty and maximal value otherwise``
|
||||
"""
|
||||
if self:
|
||||
return max(self.series)
|
||||
return None
|
||||
|
||||
@property
|
||||
def mean(self) -> float | int | None:
|
||||
"""
|
||||
get mean value in series
|
||||
|
||||
Returns:
|
||||
float | int | None: ``None`` if series is empty and mean value otherwise
|
||||
"""
|
||||
if self:
|
||||
return statistics.mean(self.series)
|
||||
return None
|
||||
|
||||
@property
|
||||
def min(self) -> float | int | None:
|
||||
"""
|
||||
get min value in series
|
||||
|
||||
Returns:
|
||||
float | int | None: ``None`` if series is empty and minimal value otherwise
|
||||
"""
|
||||
if self:
|
||||
return min(self.series)
|
||||
return None
|
||||
|
||||
@property
|
||||
def st_dev(self) -> float | None:
|
||||
"""
|
||||
get standard deviation in series
|
||||
|
||||
Returns:
|
||||
float | None: ``None`` if series size is less than 1, 0 if series contains single element and standard
|
||||
deviation otherwise
|
||||
"""
|
||||
if not self:
|
||||
return None
|
||||
if len(self.series) > 1:
|
||||
return statistics.stdev(self.series)
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
"""
|
||||
retrieve amount of elements
|
||||
|
||||
Returns:
|
||||
int: the series collection size
|
||||
"""
|
||||
return len(self.series)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""
|
||||
check if series is empty or not
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if series contains elements and ``False`` otherwise
|
||||
"""
|
||||
return bool(self.total)
|
@ -49,7 +49,6 @@ 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
|
||||
|
@ -25,6 +25,6 @@ class FileSchema(Schema):
|
||||
request file upload schema
|
||||
"""
|
||||
|
||||
archive = fields.Raw(required=True, metadata={
|
||||
archive = fields.Field(required=True, metadata={
|
||||
"description": "Package archive to be uploaded",
|
||||
})
|
||||
|
@ -21,7 +21,6 @@ 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
|
||||
|
||||
|
||||
@ -33,9 +32,6 @@ 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",
|
||||
})
|
||||
|
@ -1,43 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2025 ahriman team.
|
||||
#
|
||||
# This file is part of ahriman
|
||||
# (see https://github.com/arcan1s/ahriman).
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
from ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class RepositoryStatsSchema(Schema):
|
||||
"""
|
||||
response repository stats schema
|
||||
"""
|
||||
|
||||
bases = fields.Int(metadata={
|
||||
"description": "Amount of unique packages bases",
|
||||
"example": 2,
|
||||
})
|
||||
packages = fields.Int(metadata={
|
||||
"description": "Amount of unique packages",
|
||||
"example": 4,
|
||||
})
|
||||
archive_size = fields.Int(metadata={
|
||||
"description": "Total archive size of the packages in bytes",
|
||||
"example": 42000,
|
||||
})
|
||||
installed_size = fields.Int(metadata={
|
||||
"description": "Total installed size of the packages in bytes",
|
||||
"example": 42000000,
|
||||
})
|
@ -23,7 +23,6 @@ from ahriman import __version__
|
||||
from ahriman.models.build_status import BuildStatusEnum
|
||||
from ahriman.models.counters import Counters
|
||||
from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
from ahriman.models.user_access import UserAccess
|
||||
from ahriman.web.apispec.decorators import apidocs
|
||||
from ahriman.web.schemas import InternalStatusSchema, RepositoryIdSchema, StatusSchema
|
||||
@ -61,16 +60,12 @@ class StatusView(StatusViewGuard, BaseView):
|
||||
Response: 200 with service status object
|
||||
"""
|
||||
repository_id = self.repository_id()
|
||||
packages = self.service(repository_id).packages
|
||||
counters = Counters.from_packages(packages)
|
||||
stats = RepositoryStats.from_packages([package for package, _ in packages])
|
||||
|
||||
counters = Counters.from_packages(self.service(repository_id).packages)
|
||||
status = InternalStatus(
|
||||
status=self.service(repository_id).status,
|
||||
architecture=repository_id.architecture,
|
||||
packages=counters,
|
||||
repository=repository_id.name,
|
||||
stats=stats,
|
||||
version=__version__,
|
||||
)
|
||||
|
||||
|
@ -11,7 +11,6 @@ from ahriman.core.repository import Repository
|
||||
from ahriman.core.utils import pretty_datetime, utcnow
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
@ -41,16 +40,13 @@ def test_run(args: argparse.Namespace, configuration: Configuration, repository:
|
||||
"""
|
||||
args = _default_args(args)
|
||||
events = [Event("1", "1"), Event("2", "2")]
|
||||
stats = RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4)
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
events_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=events)
|
||||
stats_mock = mocker.patch("ahriman.core.status.client.Client.statistics", return_value=stats)
|
||||
application_mock = mocker.patch("ahriman.application.handlers.statistics.Statistics.stats_per_package")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Statistics.run(args, repository_id, configuration, report=False)
|
||||
events_mock.assert_called_once_with(args.event, args.package, None, None, args.limit, args.offset)
|
||||
stats_mock.assert_called_once_with()
|
||||
application_mock.assert_called_once_with(args.event, events, args.chart)
|
||||
|
||||
|
||||
|
@ -12,7 +12,6 @@ from ahriman.core.formatters import \
|
||||
PackageStatsPrinter, \
|
||||
PatchPrinter, \
|
||||
RepositoryPrinter, \
|
||||
RepositoryStatsPrinter, \
|
||||
StatusPrinter, \
|
||||
StringPrinter, \
|
||||
TreePrinter, \
|
||||
@ -26,7 +25,6 @@ from ahriman.models.changes import Changes
|
||||
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
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
@ -136,29 +134,12 @@ def repository_printer(repository_id: RepositoryId) -> RepositoryPrinter:
|
||||
"""
|
||||
fixture for repository printer
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository identifier fixture
|
||||
|
||||
Returns:
|
||||
RepositoryPrinter: repository printer test instance
|
||||
"""
|
||||
return RepositoryPrinter(repository_id)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repository_stats_printer(repository_id: RepositoryId) -> RepositoryStatsPrinter:
|
||||
"""
|
||||
fixture for repository stats printer
|
||||
|
||||
Args:
|
||||
repository_id(RepositoryId): repository identifier fixture
|
||||
|
||||
Returns:
|
||||
RepositoryStatsPrinter: repository stats printer test instance
|
||||
"""
|
||||
return RepositoryStatsPrinter(repository_id, RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def status_printer() -> StatusPrinter:
|
||||
"""
|
||||
|
@ -15,6 +15,13 @@ def test_properties_empty() -> None:
|
||||
assert EventStatsPrinter("event", []).properties()
|
||||
|
||||
|
||||
def test_properties_single() -> None:
|
||||
"""
|
||||
must skip calculation of the standard deviation for single event
|
||||
"""
|
||||
assert EventStatsPrinter("event", [1]).properties()
|
||||
|
||||
|
||||
def test_title(event_stats_printer: EventStatsPrinter) -> None:
|
||||
"""
|
||||
must return non-empty title
|
||||
|
@ -1,15 +0,0 @@
|
||||
from ahriman.core.formatters import RepositoryStatsPrinter
|
||||
|
||||
|
||||
def test_properties(repository_stats_printer: RepositoryStatsPrinter) -> None:
|
||||
"""
|
||||
must return non-empty properties list
|
||||
"""
|
||||
assert repository_stats_printer.properties()
|
||||
|
||||
|
||||
def test_title(repository_stats_printer: RepositoryStatsPrinter) -> None:
|
||||
"""
|
||||
must return non-empty title
|
||||
"""
|
||||
assert repository_stats_printer.title()
|
@ -16,7 +16,6 @@ from ahriman.models.internal_status import InternalStatus
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
def test_load_dummy_client(configuration: Configuration) -> None:
|
||||
@ -286,14 +285,6 @@ def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: Mock
|
||||
update_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_statistics(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly fetch statistics
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)])
|
||||
assert client.statistics() == RepositoryStats(bases=1, packages=1, archive_size=4200, installed_size=4200000)
|
||||
|
||||
|
||||
def test_status_get(client: Client) -> None:
|
||||
"""
|
||||
must return dummy status for web service
|
||||
|
@ -479,7 +479,6 @@ def test_walk(resource_path_root: Path) -> None:
|
||||
resource_path_root / "models" / "pkgbuild",
|
||||
resource_path_root / "models" / "utf8",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "alerts.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "dashboard.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "key-import-modal.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "login-modal.jinja2",
|
||||
resource_path_root / "web" / "templates" / "build-status" / "package-add-modal.jinja2",
|
||||
|
@ -14,7 +14,6 @@ from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -72,9 +71,8 @@ def internal_status(counters: Counters) -> InternalStatus:
|
||||
status=BuildStatus(),
|
||||
architecture="x86_64",
|
||||
packages=counters,
|
||||
repository="aur",
|
||||
stats=RepositoryStats(bases=1, packages=2, archive_size=3, installed_size=4),
|
||||
version=__version__,
|
||||
repository="aur",
|
||||
)
|
||||
|
||||
|
||||
|
@ -68,10 +68,3 @@ def test_lt_invalid() -> None:
|
||||
"""
|
||||
with pytest.raises(ValueError):
|
||||
assert RepositoryId("x86_64", "a") < 42
|
||||
|
||||
|
||||
def test_str() -> None:
|
||||
"""
|
||||
must convert identifier to string
|
||||
"""
|
||||
assert str(RepositoryId("x86_64", "a")) == "a (x86_64)"
|
||||
|
@ -1,24 +0,0 @@
|
||||
from dataclasses import asdict
|
||||
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
def test_repository_stats_from_json_view(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
stats = RepositoryStats.from_packages([package_ahriman, package_python_schedule])
|
||||
assert RepositoryStats.from_json(asdict(stats)) == stats
|
||||
|
||||
|
||||
def test_from_packages(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must generate stats from packages list
|
||||
"""
|
||||
assert RepositoryStats.from_packages([package_ahriman, package_python_schedule]) == RepositoryStats(
|
||||
bases=2,
|
||||
packages=3,
|
||||
archive_size=12603,
|
||||
installed_size=12600003,
|
||||
)
|
@ -1,80 +0,0 @@
|
||||
from ahriman.models.series_statistics import SeriesStatistics
|
||||
|
||||
|
||||
def test_max() -> None:
|
||||
"""
|
||||
must return maximal value
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).max == 3
|
||||
|
||||
|
||||
def test_max_empty() -> None:
|
||||
"""
|
||||
must return None as maximal value if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).max is None
|
||||
|
||||
|
||||
def test_mean() -> None:
|
||||
"""
|
||||
must return mean value
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).mean == 2
|
||||
|
||||
|
||||
def test_mean_empty() -> None:
|
||||
"""
|
||||
must return None as mean value if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).mean is None
|
||||
|
||||
|
||||
def test_min() -> None:
|
||||
"""
|
||||
must return minimal value
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).min == 1
|
||||
|
||||
|
||||
def test_min_empty() -> None:
|
||||
"""
|
||||
must return None as minimal value if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).min is None
|
||||
|
||||
|
||||
def test_st_dev() -> None:
|
||||
"""
|
||||
must return standard deviation
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).st_dev == 1
|
||||
|
||||
|
||||
def test_st_dev_empty() -> None:
|
||||
"""
|
||||
must return None as standard deviation if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).st_dev is None
|
||||
|
||||
|
||||
def test_st_dev_single() -> None:
|
||||
"""
|
||||
must return 0 as standard deviation if series contains only one element
|
||||
"""
|
||||
assert SeriesStatistics([1]).st_dev == 0
|
||||
|
||||
|
||||
def test_total() -> None:
|
||||
"""
|
||||
must return size of collection
|
||||
"""
|
||||
assert SeriesStatistics([1]).total == 1
|
||||
assert SeriesStatistics([]).total == 0
|
||||
|
||||
|
||||
def test_bool() -> None:
|
||||
"""
|
||||
must correctly define empty collection
|
||||
"""
|
||||
assert SeriesStatistics([1])
|
||||
assert not SeriesStatistics([])
|
@ -1 +0,0 @@
|
||||
# schema testing goes in view class tests
|
Reference in New Issue
Block a user