Compare commits

..

4 Commits

Author SHA1 Message Date
6b993cca3b refine package logging 2025-02-06 12:31:05 +02:00
c6cb4a4abd fix: force dry run build on task initialization for VCS packages
Previously if package is VCS and version in PKGBUILD doesn't match to
AUR one, then makepkg will update pkgbuild ignoring all previous pkgrel
patches

With this change during task init dry ryn process is always run for vcs
packages
2025-01-27 15:46:41 +02:00
22ac5e8b43 feat: add dashboard (#139) 2025-01-13 14:32:40 +02:00
15ca143b70 feat: add counters to repository stats overview 2025-01-09 17:18:50 +02:00
137 changed files with 1268 additions and 2362 deletions

View File

@ -8,10 +8,6 @@ on:
- '*'
- '!*rc*'
permissions:
contents: read
packages: write
jobs:
docker-image:

View File

@ -2,9 +2,6 @@ name: Regress
on: workflow_dispatch
permissions:
contents: read
jobs:
run-regress-tests:

View File

@ -5,9 +5,6 @@ on:
tags:
- '*'
permissions:
contents: write
jobs:
make-release:

View File

@ -8,9 +8,6 @@ on:
branches:
- master
permissions:
contents: read
jobs:
run-setup-minimal:

View File

@ -10,9 +10,6 @@ on:
schedule:
- cron: 1 0 * * *
permissions:
contents: read
jobs:
run-tests:

View File

@ -9,7 +9,13 @@ build:
python:
install:
- requirements: docs/requirements.txt
- method: pip
path: .
extra_requirements:
- docs
- s3
- validator
- web
formats:
- pdf

View File

@ -80,7 +80,7 @@ Again, the most checks can be performed by `tox` command, though some additional
>>> clazz = Clazz()
"""
CLAZZ_ATTRIBUTE: ClassVar[int] = 42
CLAZZ_ATTRIBUTE = 42
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
@ -96,7 +96,6 @@ 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

File diff suppressed because it is too large Load Diff

View File

@ -124,14 +124,6 @@ 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
---------------

View File

@ -100,14 +100,6 @@ 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
-------------------------------------

View File

@ -116,14 +116,6 @@ 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
---------------------------------------
@ -300,6 +292,14 @@ 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
-----------------------------------------

View File

@ -12,14 +12,6 @@ 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
---------------------------------------

View File

@ -15,8 +15,9 @@ import sys
from pathlib import Path
from ahriman import __version__
# support package imports
basedir = Path(__file__).resolve().parent.parent / "src"
sys.path.insert(0, str(basedir))
@ -28,7 +29,6 @@ copyright = f"2021-{datetime.date.today().year}, ahriman team"
author = "ahriman team"
# The full version, including alpha/beta/rc tags
from ahriman import __version__
release = __version__
@ -91,13 +91,7 @@ autoclass_content = "both"
autodoc_member_order = "groupwise"
autodoc_mock_imports = [
"aioauth_client",
"aiohttp_security",
"aiohttp_session",
"cryptography",
"pyalpm",
]
autodoc_mock_imports = ["cryptography", "pyalpm"]
autodoc_default_options = {
"no-undoc-members": True,

View File

@ -81,7 +81,6 @@ Base configuration settings.
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually.
* ``database`` - path to the application SQLite database, string, required.
* ``include`` - path to directory with configuration files overrides, string, optional. Files will be read in alphabetical order.
* ``keep_last_logs`` - amount of build logs to be kept for each package, integer, optional ,default ``0``. Logs will be cleared at the end of each process.
* ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
``alpm:*`` groups
@ -218,7 +217,7 @@ Mirrorlist generator plugin
``remote-pull`` group
---------------------
Remote git source synchronization settings. Unlike ``upload`` triggers those triggers are used for PKGBUILD synchronization - fetch from remote repository PKGBUILDs before updating process.
Remote git source synchronization settings. Unlike ``Upload`` triggers those triggers are used for PKGBUILD synchronization - fetch from remote repository PKGBUILDs before updating process.
It supports authorization; to do so you'd need to prefix the URL with authorization part, e.g. ``https://key:token@github.com/arcan1s/ahriman.git``. It is highly recommended to use application tokens instead of your user authorization details. Alternatively, you can use any other option supported by git, e.g.:

View File

@ -56,13 +56,6 @@ Though originally I've created ahriman by trying to improve the project, it stil
It is automation tools for ``repoctl`` mentioned above. Except for using shell it looks pretty cool and also offers some additional features like patches, remote synchronization (isn't it?) and reporting.
`AURCache <https://github.com/Lukas-Heiligenbrunner/AURCache>`__
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
That's really cool project if you are looking for simple service to build AUR packages. It provides very informative dashboard and easy to configure and use. However, it doesn't provide direct way to control build process (e.g. it is neither trivial to build packages for architectures which are not supported by default nor to change build flags).
Also this application relies on docker setup (e.g. builders are only available as special docker containers). In addition, it uses ``paru`` to build packages instead of ``devtools``.
How to check service logs
^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -1,128 +0,0 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --group ../pyproject.toml:docs --extra s3 --extra validator --extra web --output-file ../docs/requirements.txt ../pyproject.toml
aiohappyeyeballs==2.6.1
# via aiohttp
aiohttp==3.11.18
# via
# ahriman (../pyproject.toml)
# aiohttp-cors
# aiohttp-jinja2
aiohttp-cors==0.8.1
# via ahriman (../pyproject.toml)
aiohttp-jinja2==1.6
# via ahriman (../pyproject.toml)
aiosignal==1.3.2
# via aiohttp
alabaster==1.0.0
# via sphinx
argparse-manpage==4.6
# via ahriman (../pyproject.toml:docs)
attrs==25.3.0
# via aiohttp
babel==2.17.0
# via sphinx
bcrypt==4.3.0
# via ahriman (../pyproject.toml)
boto3==1.38.11
# via ahriman (../pyproject.toml)
botocore==1.38.11
# via
# boto3
# s3transfer
cerberus==1.3.7
# via ahriman (../pyproject.toml)
certifi==2025.4.26
# via requests
charset-normalizer==3.4.2
# via requests
docutils==0.21.2
# via
# sphinx
# sphinx-argparse
# sphinx-rtd-theme
frozenlist==1.6.0
# via
# aiohttp
# aiosignal
idna==3.10
# via
# requests
# yarl
imagesize==1.4.1
# via sphinx
inflection==0.5.1
# via ahriman (../pyproject.toml)
jinja2==3.1.6
# via
# aiohttp-jinja2
# sphinx
jmespath==1.0.1
# via
# boto3
# botocore
markupsafe==3.0.2
# via jinja2
multidict==6.4.3
# via
# aiohttp
# yarl
packaging==25.0
# via sphinx
propcache==0.3.1
# via
# aiohttp
# yarl
pydeps==3.0.1
# via ahriman (../pyproject.toml:docs)
pyelftools==0.32
# via ahriman (../pyproject.toml)
pygments==2.19.1
# via sphinx
python-dateutil==2.9.0.post0
# via botocore
requests==2.32.3
# via
# ahriman (../pyproject.toml)
# sphinx
roman-numerals-py==3.1.0
# via sphinx
s3transfer==0.12.0
# via boto3
shtab==1.7.2
# via ahriman (../pyproject.toml:docs)
six==1.17.0
# via python-dateutil
snowballstemmer==3.0.0.1
# via sphinx
sphinx==8.2.3
# via
# ahriman (../pyproject.toml:docs)
# sphinx-argparse
# sphinx-rtd-theme
# sphinxcontrib-jquery
sphinx-argparse==0.5.2
# via ahriman (../pyproject.toml:docs)
sphinx-rtd-theme==3.0.2
# via ahriman (../pyproject.toml:docs)
sphinxcontrib-applehelp==2.0.0
# via sphinx
sphinxcontrib-devhelp==2.0.0
# via sphinx
sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jquery==4.1
# via sphinx-rtd-theme
sphinxcontrib-jsmath==1.0.1
# via sphinx
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
stdlib-list==0.11.1
# via pydeps
urllib3==2.4.0
# via
# botocore
# requests
yarl==1.20.0
# via aiohttp

View File

@ -12,22 +12,19 @@ Initial setup
sudo ahriman -a x86_64 -r aur service-setup ...
.. admonition:: Details
:collapsible: closed
``service-setup`` literally does the following steps:
``service-setup`` literally does the following steps:
#.
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
#.
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
.. code-block:: shell
.. code-block:: shell
echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
#.
Configure build tools (it is required for correct dependency management system):
#.
Configure build tools (it is required for correct dependency management system):
#.
#.
Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``):
.. code-block:: shell
@ -70,7 +67,7 @@ Initial setup
echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /etc/sudoers.d/ahriman
This command supports several arguments, kindly refer to its help message.
This command supports several arguments, kindly refer to its help message.
#.
Start and enable ``ahriman@.timer`` via ``systemctl``:

View File

@ -2,7 +2,7 @@
pkgbase='ahriman'
pkgname=('ahriman' 'ahriman-core' 'ahriman-triggers' 'ahriman-web')
pkgver=2.18.1
pkgver=2.17.1
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')

View File

@ -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" aria-controls="repositories-navbar" aria-expanded="false" aria-label="Toggle navigation">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#repositories-navbar-supported-content" aria-controls="repositories-navbar-supported-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="repositories-navbar" class="collapse navbar-collapse">
<div id="repositories-navbar-supported-content" class="collapse navbar-collapse">
<ul id="repositories" class="nav nav-tabs">
{% for repository in repositories %}
<li class="nav-item">

View File

@ -59,17 +59,7 @@
</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">
<div class="row">
<div class="col-1 dropend">
<button id="package-info-logs-dropdown" class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-list"></i>
</button>
<nav id="package-info-logs-versions" class="dropdown-menu" aria-labelledby="package-info-logs-dropdown"></nav>
</div>
<div class="col-11">
<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>
<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 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>
@ -110,7 +100,6 @@
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");
@ -296,45 +285,11 @@
convert: response => response.json(),
},
data => {
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("dropdown-item");
link.textContent = new Date(1000 * version.created).toISOStringShort();
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();
const logs = data.map(log_record => {
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
});
packageInfoLogsInput.textContent = logs.join("\n");
highlight(packageInfoLogsInput);
},
onFailure,
);

View File

@ -635,7 +635,6 @@ _set_new_action() {
# ${!x} -> ${hello} -> "world"
_shtab_ahriman() {
local completing_word="${COMP_WORDS[COMP_CWORD]}"
local previous_word="${COMP_WORDS[COMP_CWORD-1]}"
local completed_positional_actions
local current_action
local current_action_args_start_index
@ -692,10 +691,6 @@ _shtab_ahriman() {
if [[ $pos_only = 0 && "${completing_word}" == -* ]]; then
# optional argument started: use option strings
COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") )
elif [[ "${previous_word}" == ">" || "${previous_word}" == ">>" ||
"${previous_word}" =~ ^[12]">" || "${previous_word}" =~ ^[12]">>" ]]; then
# handle redirection operators
COMPREPLY=( $(compgen -f -- "${completing_word}") )
else
# use choices & compgen
local IFS=$'\n' # items may contain spaces, so delimit using newline

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2025\-06\-16" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2025\-01\-05" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS

View File

@ -25,64 +25,15 @@ dependencies = [
dynamic = ["version"]
[project.optional-dependencies]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
reports = [
"Jinja2",
]
s3 = [
"boto3",
]
shell = [
"IPython"
]
stats = [
"matplotlib",
]
unixsocket = [
"requests-unixsocket2", # required by unix socket support
]
validator = [
"cerberus",
]
web = [
"aiohttp",
"aiohttp_cors",
"aiohttp_jinja2",
]
web_api-docs = [
"ahriman[web]",
"aiohttp-apispec",
"setuptools", # required by aiohttp-apispec
]
web_auth = [
"ahriman[web]",
"aiohttp_session",
"aiohttp_security",
"cryptography",
]
web_oauth2 = [
"ahriman[web_auth]",
"aioauth-client",
]
[project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.urls]
Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases"
[dependency-groups]
[project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.optional-dependencies]
check = [
"autopep8",
"bandit",
@ -96,6 +47,24 @@ docs = [
"shtab",
"sphinx-argparse",
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734
]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
s3 = [
"boto3",
]
shell = [
"IPython"
]
stats = [
"matplotlib",
]
tests = [
"pytest",
@ -106,6 +75,22 @@ tests = [
"pytest-resource-path",
"pytest-spec",
]
validator = [
"cerberus",
]
web = [
"Jinja2",
"aioauth-client",
"aiohttp",
"aiohttp-apispec",
"aiohttp_cors",
"aiohttp_jinja2",
"aiohttp_session",
"aiohttp_security",
"cryptography",
"requests-unixsocket2", # required by unix socket support
"setuptools", # required by aiohttp-apispec
]
[tool.flit.sdist]
include = [

View File

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

View File

@ -117,7 +117,7 @@ class Application(ApplicationPackages, ApplicationRepository):
Args:
packages(list[Package]): list of source packages of which dependencies have to be processed
process_dependencies(bool): if set to ``False``, dependencies will not be processed
process_dependencies(bool): if no set, dependencies will not be processed
Returns:
list[Package]: updated packages list. Packager for dependencies will be copied from the original package
@ -130,9 +130,6 @@ class Application(ApplicationPackages, ApplicationRepository):
>>> packages = application.with_dependencies(packages, process_dependencies=True)
>>> application.print_updates(packages, log_fn=print)
"""
if not process_dependencies or not packages:
return packages
def missing_dependencies(source: Iterable[Package]) -> dict[str, str | None]:
# append list of known packages with packages which are in current sources
satisfied_packages = known_packages | {
@ -148,29 +145,22 @@ class Application(ApplicationPackages, ApplicationRepository):
if dependency not in satisfied_packages
}
def new_packages(root: Package) -> dict[str, Package]:
portion = {root.base: root}
while missing := missing_dependencies(portion.values()):
for package_name, packager in missing.items():
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
# there is local cache, load package from it
leaf = Package.from_build(source_dir, self.repository.architecture, packager)
else:
leaf = Package.from_aur(package_name, packager)
portion[leaf.base] = leaf
# register package in the database
self.repository.reporter.set_unknown(leaf)
return portion
if not process_dependencies or not packages:
return packages
known_packages = self._known_packages()
with_dependencies: dict[str, Package] = {}
for package in packages:
with self.in_package_context(package.base, package.version): # use the same context for the logger
try:
with_dependencies |= new_packages(package)
except Exception:
self.logger.exception("could not process dependencies of %s, skip the package", package.base)
with_dependencies = {package.base: package for package in packages}
while missing := missing_dependencies(with_dependencies.values()):
for package_name, username in missing.items():
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
# there is local cache, load package from it
package = Package.from_build(source_dir, self.repository.architecture, username)
else:
package = Package.from_aur(package_name, username)
with_dependencies[package.base] = package
# register package in the database
self.repository.reporter.set_unknown(package)
return list(with_dependencies.values())

View File

@ -22,7 +22,7 @@ import logging
from collections.abc import Callable, Iterable
from multiprocessing import Pool
from typing import ClassVar, TypeVar
from typing import TypeVar
from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration
@ -53,13 +53,13 @@ class Handler:
Wrapper for all command line actions, though each derived class implements :func:`run()` method, it usually
must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
>>> from ahriman.application.handlers.add import Add
>>> from ahriman.application.handlers import Add
>>>
>>> Add.execute(args)
"""
ALLOW_MULTI_ARCHITECTURE_RUN: ClassVar[bool] = True
arguments: ClassVar[list[Callable[[SubParserAction], argparse.ArgumentParser]]]
ALLOW_MULTI_ARCHITECTURE_RUN = True
arguments: list[Callable[[SubParserAction], argparse.ArgumentParser]]
@classmethod
def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool:

View File

@ -21,7 +21,6 @@ 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
@ -41,7 +40,7 @@ class Search(Handler):
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
SORT_FIELDS: ClassVar[set[str]] = {
SORT_FIELDS = {
field.name
for field in fields(AURPackage)
if field.default_factory is not list

View File

@ -21,7 +21,6 @@ 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
@ -47,9 +46,9 @@ class Setup(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
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"
ARCHBUILD_COMMAND_PATH = Path("/") / "usr" / "bin" / "archbuild"
MIRRORLIST_PATH = Path("/") / "etc" / "pacman.d" / "mirrorlist"
SUDOERS_DIR_PATH = Path("/") / "etc" / "sudoers.d"
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -23,7 +23,6 @@ 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
@ -37,11 +36,11 @@ class Versions(Handler):
version handler
Attributes:
PEP423_PACKAGE_NAME(re.Pattern[str]): (class attribute) special regex for valid PEP423 package name
PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name
"""
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
PEP423_PACKAGE_NAME: ClassVar[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9._-]+")
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+")
@classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -23,7 +23,6 @@ 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
@ -42,7 +41,7 @@ class PacmanDatabase(SyncHttpClient):
sync_files_database(bool): sync files database
"""
LAST_MODIFIED_HEADER: ClassVar[str] = "Last-Modified"
LAST_MODIFIED_HEADER = "Last-Modified"
def __init__(self, database: DB, configuration: Configuration) -> None:
"""

View File

@ -34,14 +34,14 @@ class PkgbuildToken(StrEnum):
well-known tokens dictionary
Attributes:
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
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
"""
ArrayStarts = "("

View File

@ -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, ClassVar
from typing import Any
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: ClassVar[str] = "https://aur.archlinux.org"
DEFAULT_RPC_URL: ClassVar[str] = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION: ClassVar[str] = "5"
DEFAULT_AUR_URL = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5"
@classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str:

View File

@ -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, ClassVar
from typing import Any
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: 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"
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"
@classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str:

View File

@ -35,7 +35,7 @@ class Remote(SyncHttpClient):
>>> package = AUR.info("ahriman")
>>> search_result = Official.multisearch("pacman", "manager", pacman=pacman)
Difference between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to
Differnece between :func:`search()` and :func:`multisearch()` is that :func:`search()` passes all arguments to
underlying wrapper directly, whereas :func:`multisearch()` splits search one by one and finds intersection
between search results.
"""

View File

@ -21,7 +21,6 @@ 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
@ -43,9 +42,9 @@ class Sources(LazyLogging):
GITCONFIG(dict[str, str]): (class attribute) git config options to suppress annoying hints
"""
DEFAULT_BRANCH: ClassVar[str] = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR: ClassVar[tuple[str, str]] = ("ahriman", "ahriman@localhost")
GITCONFIG: ClassVar[dict[str, str]] = {
DEFAULT_BRANCH = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
GITCONFIG = {
"init.defaultBranch": DEFAULT_BRANCH,
}

View File

@ -22,7 +22,7 @@ import shlex
import sys
from pathlib import Path
from typing import Any, ClassVar, Self
from typing import Any, 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: ClassVar[list[str]] = ["alpm", "build", "sign"]
SYSTEM_CONFIGURATION_PATH: ClassVar[Path] = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None:
"""

View File

@ -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):
case [current_value] | str(current_value): # type: ignore[misc]
value = f"{current_value} {value}"
case None:
pass

View File

@ -57,6 +57,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"path_exists": True,
"path_type": "file",
},
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",
}
},
},
"alpm": {
@ -343,6 +347,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"coerce": "integer",
"min": 0,
},
"password": {
"type": "string",
"empty": False,
},
"port": {
"type": "integer",
"coerce": "integer",
@ -371,6 +379,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
},
"empty": False,
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"unix_socket": {
"type": "path",
"coerce": "absolute_path",
@ -379,6 +392,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"type": "boolean",
"coerce": "boolean",
},
"username": {
"type": "string",
"empty": False,
},
"wait_timeout": {
"type": "integer",
"coerce": "integer",

View File

@ -23,7 +23,6 @@ import sys
from collections.abc import Generator, Mapping, MutableMapping
from string import Template
from typing import Any, ClassVar
from ahriman.core.configuration.shell_template import ShellTemplate
@ -33,7 +32,7 @@ class ShellInterpolator(configparser.Interpolation):
custom string interpolator, because we cannot use defaults argument due to config validation
"""
DATA_LINK_ESCAPE: ClassVar[str] = "\x10"
DATA_LINK_ESCAPE = "\x10"
@staticmethod
def _extract_variables(parser: MutableMapping[str, Mapping[str, str]], value: str,
@ -85,7 +84,7 @@ class ShellInterpolator(configparser.Interpolation):
"prefix": sys.prefix,
}
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: Any, option: Any, value: str,
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str,
defaults: Mapping[str, str]) -> str:
"""
interpolate option value
@ -100,8 +99,8 @@ class ShellInterpolator(configparser.Interpolation):
Args:
parser(MutableMapping[str, Mapping[str, str]]): option parser
section(Any): section name
option(Any): option name
section(str): section name
option(str): option name
value(str): source (not-converted) value
defaults(Mapping[str, str]): default values

View File

@ -28,6 +28,9 @@ 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][^}]*)"

View File

@ -62,31 +62,24 @@ class Migrations(LazyLogging):
"""
return Migrations(connection, configuration).run()
def apply_migrations(self, migrations: list[Migration]) -> None:
def migration(self, cursor: Cursor, migration: Migration) -> None:
"""
perform migrations explicitly
perform single migration
Args:
migrations(list[Migration]): list of migrations to perform
cursor(Cursor): connection cursor
migration(Migration): single migration to perform
"""
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
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 migrations(self) -> list[Migration]:
"""
@ -121,25 +114,6 @@ 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
@ -148,7 +122,6 @@ 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)
@ -157,8 +130,25 @@ class Migrations(LazyLogging):
self.logger.info("no migrations required")
return result
self.apply_migrations(migrations[current_version:])
self.connection.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
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.logger.info("migrations have been performed from version %s to %s", result.old_version, result.new_version)
return result

View File

@ -24,7 +24,4 @@ steps = [
"""
alter table logs add column process_id text not null default ''
""",
"""
alter table logs rename column record to message
""",
]

View File

@ -20,7 +20,7 @@
from sqlite3 import Connection
from ahriman.core.database.operations.operations import Operations
from ahriman.models.log_record import LogRecord
from ahriman.models.log_record_id import LogRecordId
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[LogRecord]:
repository_id: RepositoryId | None = None) -> list[tuple[LogRecordId, float, str]]:
"""
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[LogRecord]: sorted package log records
list[tuple[LogRecordId, float, str]]: sorted package log records and their timestamps
"""
repository_id = repository_id or self._repository_id
def run(connection: Connection) -> list[LogRecord]:
def run(connection: Connection) -> list[tuple[LogRecordId, float, str]]:
return [
LogRecord.from_json(package_base, row)
(LogRecordId(package_base, row["version"], row["process_id"]), row["created"], row["record"])
for row in connection.execute(
"""
select created, message, version, process_id from (
select created, record, version, process_id from (
select * from logs
where package_base = :package_base and repository = :repository
order by created desc limit :limit offset :offset
@ -66,12 +66,15 @@ class LogsOperations(Operations):
return self.with_connection(run)
def logs_insert(self, log_record: LogRecord, repository_id: RepositoryId | None = None) -> None:
def logs_insert(self, log_record_id: LogRecordId, created: float, record: str,
repository_id: RepositoryId | None = None) -> None:
"""
write new log record to database
Args:
log_record(LogRecord): log record object
log_record_id(LogRecordId): current log record id
created(float): log created timestamp from log record attribute
record(str): log record
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
"""
repository_id = repository_id or self._repository_id
@ -80,14 +83,18 @@ class LogsOperations(Operations):
connection.execute(
"""
insert into logs
(package_base, version, created, message, repository, process_id)
(package_base, version, created, record, repository, process_id)
values
(:package_base, :version, :created, :message, :repository, :process_id)
(:package_base, :version, :created, :record, :repository, :process_id)
""",
{
"package_base": log_record.log_record_id.package_base,
"package_base": log_record_id.package_base,
"version": log_record_id.version,
"created": created,
"record": record,
"repository": repository_id.id,
} | log_record.view()
"process_id": log_record_id.process_id,
}
)
return self.with_connection(run, commit=True)

View File

@ -17,7 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import contextlib
import sqlite3
from collections.abc import Callable
@ -88,12 +87,10 @@ class Operations(LazyLogging):
Returns:
T: result of the ``query`` call
"""
with contextlib.closing(sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES)) as connection:
with sqlite3.connect(self.path, detect_types=sqlite3.PARSE_DECLTYPES) as connection:
connection.set_trace_callback(self.logger.debug)
connection.row_factory = self.factory
result = query(connection)
if commit:
connection.commit()
return result

View File

@ -22,6 +22,7 @@ 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
@ -33,7 +34,7 @@ class DistributedSystem(Trigger, WebClient):
simple class to (un)register itself as a distributed worker
"""
CONFIGURATION_SCHEMA = {
CONFIGURATION_SCHEMA: ConfigurationSchema = {
"worker": {
"type": "dict",
"schema": {

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import ClassVar
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.property import Property
@ -33,7 +31,7 @@ class ConfigurationPrinter(StringPrinter):
values(dict[str, str]): configuration values dictionary
"""
HIDE_KEYS: ClassVar[list[str]] = [
HIDE_KEYS = [
"api_key", # telegram key
"client_secret", # oauth secret
"cookie_secret_key", # cookie secret key

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import ClassVar
from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.property import Property
@ -28,11 +26,10 @@ 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: ClassVar[int] = 10
MAX_COUNT = 10
def __init__(self, events: dict[str, int]) -> None:
"""

View File

@ -19,14 +19,11 @@
#
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
@ -82,7 +79,6 @@ 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
@ -99,7 +95,7 @@ class HttpLogHandler(logging.Handler):
return # in case if no package base supplied we need just skip log message
try:
self.reporter.package_logs_add(LogRecord(log_record_id, record.created, record.getMessage()))
self.reporter.package_logs_add(log_record_id, record.created, record.getMessage())
except Exception:
if self.suppress_errors:
return

View File

@ -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 "<unknown>")
record.package_id = LogRecordId(package_base, version or "")
return record
logging.setLogRecordFactory(package_record_factory)

View File

@ -21,7 +21,6 @@ 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
@ -39,9 +38,9 @@ class LogLoader:
DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device
"""
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"
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"
@staticmethod
def handler(selected: LogHandler | None) -> LogHandler:

View File

@ -40,7 +40,7 @@ class JinjaTemplate:
* homepage - link to homepage, string, optional
* last_update - report generation time, pretty printed datetime, required
* link_path - prefix of packages to download, string, required
* link_path - prefix fo packages to download, string, required
* has_package_signed - ``True`` in case if package sign enabled, ``False`` otherwise, required
* has_repo_signed - ``True`` in case if repository database sign enabled, ``False`` otherwise, required
* packages - sorted list of packages properties, required
@ -64,7 +64,7 @@ class JinjaTemplate:
Attributes:
default_pgp_key(str | None): default PGP key
homepage(str | None): homepage link if any (for footer)
link_path(str): prefix of packages to download
link_path(str): prefix fo packages to download
name(str): repository name
rss_url(str | None): link to the RSS feed
sign_targets(set[SignSettings]): targets to sign enabled in configuration

View File

@ -67,6 +67,14 @@ class ReportTrigger(Trigger):
"type": "string",
"allowed": ["email"],
},
"full_template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template_full"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"homepage": {
"type": "string",
"empty": False,
@ -124,16 +132,26 @@ class ReportTrigger(Trigger):
},
"template": {
"type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"],
"required": True,
"empty": False,
},
"template_full": {
"type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"],
"required": True,
"empty": False,
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"templates": {
"type": "list",
"coerce": "list",
@ -181,10 +199,19 @@ class ReportTrigger(Trigger):
},
"template": {
"type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"],
"required": True,
"empty": False,
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"templates": {
"type": "list",
"coerce": "list",
@ -198,6 +225,76 @@ class ReportTrigger(Trigger):
},
},
},
"telegram": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["telegram"],
},
"api_key": {
"type": "string",
"required": True,
"empty": False,
},
"chat_id": {
"type": "string",
"required": True,
"empty": False,
},
"homepage": {
"type": "string",
"empty": False,
"is_url": ["http", "https"],
},
"link_path": {
"type": "string",
"required": True,
"empty": False,
"is_url": [],
},
"rss_url": {
"type": "string",
"empty": False,
"is_url": ["http", "https"],
},
"template": {
"type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"],
"required": True,
"empty": False,
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"template_type": {
"type": "string",
"allowed": ["MarkdownV2", "HTML", "Markdown"],
},
"templates": {
"type": "list",
"coerce": "list",
"schema": {
"type": "path",
"coerce": "absolute_path",
"path_exists": True,
"path_type": "dir",
},
"empty": False,
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
},
"remote-call": {
"type": "dict",
"schema": {
@ -257,10 +354,19 @@ class ReportTrigger(Trigger):
},
"template": {
"type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"],
"required": True,
"empty": False,
},
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"templates": {
"type": "list",
"coerce": "list",
@ -274,67 +380,6 @@ class ReportTrigger(Trigger):
},
},
},
"telegram": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["telegram"],
},
"api_key": {
"type": "string",
"required": True,
"empty": False,
},
"chat_id": {
"type": "string",
"required": True,
"empty": False,
},
"homepage": {
"type": "string",
"empty": False,
"is_url": ["http", "https"],
},
"link_path": {
"type": "string",
"required": True,
"empty": False,
"is_url": [],
},
"rss_url": {
"type": "string",
"empty": False,
"is_url": ["http", "https"],
},
"template": {
"type": "string",
"dependencies": ["templates"],
"required": True,
"empty": False,
},
"template_type": {
"type": "string",
"allowed": ["MarkdownV2", "HTML", "Markdown"],
},
"templates": {
"type": "list",
"coerce": "list",
"schema": {
"type": "path",
"coerce": "absolute_path",
"path_exists": True,
"path_type": "dir",
},
"empty": False,
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
},
}
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import ClassVar
from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient
from ahriman.core.report.jinja_template import JinjaTemplate
@ -41,8 +39,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: ClassVar[str] = "https://api.telegram.org"
TELEGRAM_MAX_CONTENT_LENGTH: ClassVar[int] = 4096
TELEGRAM_API_URL = "https://api.telegram.org"
TELEGRAM_MAX_CONTENT_LENGTH = 4096
def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
"""

View File

@ -71,7 +71,7 @@ class EventLogger:
>>> with self.in_event(package_base, EventType.PackageUpdated):
>>> do_something()
Additional parameter ``failure`` can be set in order to emit an event on exception occurred. If none set
Additional parameter ``failure`` can be set in order to emit an event on exception occured. If none set
(default), then no event will be recorded on exception
"""
with MetricsTimer() as timer:

View File

@ -75,7 +75,7 @@ class Executor(PackageInfo, Cleaner):
result = Result()
for single in updates:
with self.in_package_context(single.base, local_versions.get(single.base)), \
with self.in_package_context(single.base, single.version), \
TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
try:
with self.in_event(single.base, EventType.PackageUpdated, failure=EventType.PackageUpdateFailed):
@ -194,7 +194,6 @@ class Executor(PackageInfo, Cleaner):
self.repo.add(package_path)
current_packages = {package.base: package for package in self.packages()}
local_versions = {package_base: package.version for package_base, package in current_packages.items()}
removed_packages: list[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
@ -202,7 +201,7 @@ class Executor(PackageInfo, Cleaner):
result = Result()
for local in updates:
with self.in_package_context(local.base, local_versions.get(local.base)):
with self.in_package_context(local.base, local.version):
try:
packager = self.packager(packagers, local.base)

View File

@ -27,7 +27,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 import LogRecord
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
@ -194,16 +194,19 @@ class Client:
"""
raise NotImplementedError
def package_logs_add(self, log_record: LogRecord) -> None:
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
post log record
Args:
log_record(LogRecord): log record
log_record_id(LogRecordId): log record id
created(float): log created timestamp
message(str): log message
"""
# 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[LogRecord]:
def package_logs_get(self, package_base: str, limit: int = -1,
offset: int = 0) -> list[tuple[LogRecordId, float, str]]:
"""
get package logs
@ -213,7 +216,7 @@ class Client:
offset(int, optional): records offset (Default value = 0)
Returns:
list[LogRecord]: package logs
list[tuple[LogRecordId, float, str]]: package logs
Raises:
NotImplementedError: not implemented method

View File

@ -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 import LogRecord
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
@ -143,16 +143,19 @@ 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: LogRecord) -> None:
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
post log record
Args:
log_record(LogRecord): log record
log_record_id(LogRecordId): log record id
created(float): log created timestamp
message(str): log message
"""
self.database.logs_insert(log_record, self.repository_id)
self.database.logs_insert(log_record_id, created, message, self.repository_id)
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
def package_logs_get(self, package_base: str, limit: int = -1,
offset: int = 0) -> list[tuple[LogRecordId, float, str]]:
"""
get package logs
@ -162,7 +165,7 @@ class LocalClient(Client):
offset(int, optional): records offset (Default value = 0)
Returns:
list[LogRecord]: package logs
list[tuple[LogRecordId, float, str]]: package logs
"""
return self.database.logs_get(package_base, limit, offset, self.repository_id)

View File

@ -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 import LogRecord
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -107,9 +107,9 @@ class Watcher(LazyLogging):
except KeyError:
raise UnknownPackageError(package_base) from None
package_logs_add: Callable[[LogRecord], None]
package_logs_add: Callable[[LogRecordId, float, str], None]
package_logs_get: Callable[[str, int, int], list[LogRecord]]
package_logs_get: Callable[[str, int, int], list[tuple[LogRecordId, float, str]]]
package_logs_remove: Callable[[str, str | None], None]

View File

@ -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 import LogRecord
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
@ -306,27 +306,30 @@ class WebClient(Client, SyncAhrimanClient):
return []
def package_logs_add(self, log_record: LogRecord) -> None:
def package_logs_add(self, log_record_id: LogRecordId, created: float, message: str) -> None:
"""
post log record
Args:
log_record(LogRecord): log record
log_record_id(LogRecordId): log record id
created(float): log created timestamp
message(str): log message
"""
payload = {
"created": log_record.created,
"message": log_record.message,
"process_id": log_record.log_record_id.process_id,
"version": log_record.log_record_id.version,
"created": created,
"message": message,
"process_id": log_record_id.process_id,
"version": 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.log_record_id.package_base),
self.make_request("POST", self._logs_url(log_record_id.package_base),
params=self.repository_id.query(), json=payload, suppress_errors=True)
def package_logs_get(self, package_base: str, limit: int = -1, offset: int = 0) -> list[LogRecord]:
def package_logs_get(self, package_base: str, limit: int = -1,
offset: int = 0) -> list[tuple[LogRecordId, float, str]]:
"""
get package logs
@ -336,7 +339,7 @@ class WebClient(Client, SyncAhrimanClient):
offset(int, optional): records offset (Default value = 0)
Returns:
list[LogRecord]: package logs
list[tuple[LogRecordId, float, str]]: package logs
"""
query = self.repository_id.query() + [("limit", str(limit)), ("offset", str(offset))]
@ -344,7 +347,13 @@ class WebClient(Client, SyncAhrimanClient):
response = self.make_request("GET", self._logs_url(package_base), params=query)
response_json = response.json()
return [LogRecord.from_json(package_base, record) for record in response_json]
return [
(
LogRecordId(package_base, record["version"], record["process_id"]),
record["created"],
record["message"]
) for record in response_json
]
return []

View File

@ -22,7 +22,6 @@ 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
@ -36,7 +35,7 @@ class PkgbuildGenerator:
PKGBUILD_STATIC_PROPERTIES(list[PkgbuildPatch]): (class attribute) list of default pkgbuild static properties
"""
PKGBUILD_STATIC_PROPERTIES: ClassVar[list[PkgbuildPatch]] = [
PKGBUILD_STATIC_PROPERTIES = [
PkgbuildPatch("pkgrel", "1"),
PkgbuildPatch("arch", ["any"]),
]

View File

@ -18,7 +18,6 @@
# 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
@ -57,8 +56,8 @@ class Trigger(LazyLogging):
>>> loader.on_result(Result(), [])
"""
CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {}
CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None
CONFIGURATION_SCHEMA: ConfigurationSchema = {}
CONFIGURATION_SCHEMA_FALLBACK: str | None = None
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
"""

View File

@ -83,20 +83,6 @@ class UploadTrigger(Trigger):
},
},
},
"remote-service": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["ahriman", "remote-service"],
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
},
"rsync": {
"type": "dict",
"schema": {
@ -121,6 +107,20 @@ class UploadTrigger(Trigger):
},
},
},
"remote-service": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["ahriman", "remote-service"],
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
},
"s3": {
"type": "dict",
"schema": {

View File

@ -25,9 +25,9 @@ class Action(StrEnum):
base action enumeration
Attributes:
List(Action): list available values
Remove(Action): remove everything from local storage
Update(Action): update local storage or add to
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 = "list"

View File

@ -27,10 +27,10 @@ class AuthSettings(StrEnum):
web authorization type
Attributes:
Disabled(AuthSettings): authorization is disabled
Configuration(AuthSettings): configuration based authorization
OAuth(AuthSettings): OAuth based provider
PAM(AuthSettings): PAM based provider
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 = "disabled"

View File

@ -29,11 +29,11 @@ class BuildStatusEnum(StrEnum):
build status enumeration
Attributes:
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(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 = "unknown"

View File

@ -28,10 +28,10 @@ class EventType(StrEnum):
predefined event types
Attributes:
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(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 = "package-outdated"
@ -78,7 +78,7 @@ class Event:
dump(dict[str, Any]): json dump body
Returns:
Self: event object
Self: dependencies object
"""
return cls(
event=dump["event"],

View File

@ -25,9 +25,9 @@ class LogHandler(StrEnum):
log handler as described by default configuration
Attributes:
Console(LogHandler): write logs to console
Syslog(LogHandler): write logs to syslog device /dev/null
Journald(LogHandler): write logs to journald directly
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 = "console"

View File

@ -1,76 +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
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,
}

View File

@ -19,8 +19,7 @@
#
import uuid
from dataclasses import dataclass
from typing import ClassVar
from dataclasses import dataclass, field
@dataclass(frozen=True)
@ -29,7 +28,6 @@ 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
@ -37,13 +35,7 @@ class LogRecordId:
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)
# this is not mistake, this value is kind of global identifier, which is generated
# upon the process start
process_id: str = field(default=str(uuid.uuid4()))

View File

@ -69,7 +69,7 @@ class Package(LazyLogging):
:attr:`ahriman.models.package_source.PackageSource.Archive`,
:attr:`ahriman.models.package_source.PackageSource.AUR`,
:attr:`ahriman.models.package_source.PackageSource.Local` and
:attr:`ahriman.models.package_source.PackageSource.Repository` respectively:
:attr:`ahriman.models.package_source.PackageSource.Repository` repsectively:
>>> ahriman_package = Package.from_aur("ahriman")
>>> pacman_package = Package.from_official("pacman", pacman)

View File

@ -32,13 +32,13 @@ class PackageSource(StrEnum):
package source for addition enumeration
Attributes:
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
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
Examples:
In case if source is unknown the :func:`resolve()` and the source

View File

@ -25,9 +25,9 @@ class PacmanSynchronization(IntEnum):
pacman database synchronization flag
Attributes:
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(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 = 0

View File

@ -21,7 +21,7 @@ from collections.abc import Iterator, Mapping
from dataclasses import dataclass
from io import StringIO
from pathlib import Path
from typing import Any, ClassVar, IO, Self
from typing import Any, IO, Self
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -33,14 +33,11 @@ class Pkgbuild(Mapping[str, Any]):
model and proxy for PKGBUILD properties
Attributes:
DEFAULT_ENCODINGS(str): (class attribute) default encoding to be applied on the file content
fields(dict[str, PkgbuildPatch]): PKGBUILD fields
"""
fields: dict[str, PkgbuildPatch]
DEFAULT_ENCODINGS: ClassVar[str] = "utf8"
@property
def variables(self) -> dict[str, str]:
"""
@ -57,29 +54,18 @@ class Pkgbuild(Mapping[str, Any]):
}
@classmethod
def from_file(cls, path: Path, encoding: str | None = None) -> Self:
def from_file(cls, path: Path) -> Self:
"""
parse PKGBUILD from the file
Args:
path(Path): path to the PKGBUILD file
encoding(str | None, optional): the encoding of the file (Default value = None)
Returns:
Self: constructed instance of self
Raises:
EncodeError: if encoding is unknown
"""
# read raw bytes from file
with path.open("rb") as input_file:
content = input_file.read()
# decode bytes content based on either
encoding = encoding or cls.DEFAULT_ENCODINGS
io = StringIO(content.decode(encoding, errors="backslashreplace"))
return cls.from_io(io)
with path.open(encoding="utf8") as input_file:
return cls.from_io(input_file)
@classmethod
def from_io(cls, stream: IO[str]) -> Self:

View File

@ -27,13 +27,13 @@ class ReportSettings(StrEnum):
report targets enumeration
Attributes:
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(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 = "disabled" # for testing purpose

View File

@ -20,7 +20,7 @@
from __future__ import annotations
from collections.abc import Callable, Iterable
from typing import Any, ClassVar, Self
from typing import Any, 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: ClassVar[list[str]] = [
STATUS_PRIORITIES = [
"failed",
"removed",
"updated",

View File

@ -27,9 +27,9 @@ class SignSettings(StrEnum):
sign targets enumeration
Attributes:
Disabled(SignSettings): option which generates no report for testing purpose
Packages(SignSettings): sign each package
Repository(SignSettings): sign repository database file
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 = "disabled"

View File

@ -27,9 +27,9 @@ class SmtpSSLSettings(StrEnum):
SMTP SSL mode enumeration
Attributes:
Disabled(SmtpSSLSettings): no SSL enabled
SSL(SmtpSSLSettings): use SMTP_SSL instead of normal SMTP client
STARTTLS(SmtpSSLSettings): use STARTTLS in normal SMTP client
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 = "disabled"

View File

@ -27,11 +27,11 @@ class UploadSettings(StrEnum):
remote synchronization targets enumeration
Attributes:
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(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 = "disabled" # for testing purpose

View File

@ -21,7 +21,7 @@ import bcrypt
from dataclasses import dataclass, replace
from secrets import token_urlsafe as generate_password
from typing import ClassVar, Self
from typing import 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: ClassVar[set[str]] = {"$2$", "$2a$", "$2x$", "$2y$", "$2b$"}
SUPPORTED_ALGOS = {"$2$", "$2a$", "$2x$", "$2y$", "$2b$"}
def __post_init__(self) -> None:
"""

View File

@ -27,11 +27,11 @@ class UserAccess(StrEnum):
web user access enumeration
Attributes:
Unauthorized(UserAccess): user can access specific resources which are marked as available
Unauthorized(UserAccess): (class attribute) user can access specific resources which are marked as available
without authorization (e.g. login, logout, static)
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
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
"""
Unauthorized = "unauthorized"

View File

@ -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/>.
#
import aiohttp_cors
import aiohttp_cors # type: ignore[import-untyped]
from aiohttp.web import Application
@ -36,7 +36,7 @@ def setup_cors(application: Application) -> aiohttp_cors.CorsConfig:
aiohttp_cors.CorsConfig: generated CORS configuration
"""
cors = aiohttp_cors.setup(application, defaults={
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
"*": aiohttp_cors.ResourceOptions(
expose_headers="*",
allow_headers="*",
allow_methods="*",

View File

@ -39,7 +39,7 @@ class RemoteSchema(Schema):
"example": ".",
})
source = fields.Enum(PackageSource, by_value=True, required=True, metadata={
"description": "Package source",
"description": "Pacakge source",
})
web_url = fields.String(metadata={
"description": "Package repository page",

View File

@ -19,7 +19,7 @@
#
import aiohttp_jinja2
from typing import Any, ClassVar
from typing import Any
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: ClassVar[UserAccess] = UserAccess.Unauthorized
GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/api-docs"]
@classmethod

View File

@ -19,7 +19,6 @@
#
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
@ -36,7 +35,7 @@ class SwaggerView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/api-docs/swagger.json"]
@classmethod

View File

@ -18,9 +18,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
from aiohttp_cors import CorsViewMixin
from aiohttp_cors import CorsViewMixin # type: ignore[import-untyped]
from collections.abc import Awaitable, Callable
from typing import ClassVar, TypeVar
from typing import 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: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES: ClassVar[list[str]] = []
OPTIONS_PERMISSION = UserAccess.Unauthorized
ROUTES: list[str] = []
@property
def configuration(self) -> Configuration:

View File

@ -19,7 +19,7 @@
#
import aiohttp_jinja2
from typing import Any, ClassVar
from typing import Any
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: ClassVar[UserAccess] = UserAccess.Unauthorized
GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/", "/index.html"]
@aiohttp_jinja2.template("build-status.jinja2")

View File

@ -18,7 +18,6 @@
# 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
@ -32,7 +31,7 @@ class StaticView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
GET_PERMISSION = UserAccess.Unauthorized
ROUTES = ["/favicon.ico"]
async def get(self) -> None:

View File

@ -17,8 +17,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from typing import ClassVar
from ahriman.core.configuration import Configuration
@ -27,7 +25,7 @@ class StatusViewGuard:
helper for check if status routes are enabled
"""
ROUTES: ClassVar[list[str]]
ROUTES: list[str]
@classmethod
def routes(cls, configuration: Configuration) -> list[str]:

View File

@ -18,7 +18,6 @@
# 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
@ -36,7 +35,7 @@ class EventsView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
GET_PERMISSION = POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/events"]
@apidocs(

View File

@ -19,7 +19,6 @@
#
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
@ -38,7 +37,7 @@ class WorkersView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/distributed"]
@apidocs(

View File

@ -18,7 +18,6 @@
# 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
@ -37,8 +36,8 @@ class ChangesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/changes"]
@apidocs(

View File

@ -18,7 +18,6 @@
# 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
@ -37,8 +36,8 @@ class DependenciesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/dependencies"]
@apidocs(

View File

@ -18,11 +18,10 @@
# 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 import LogRecord
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
from ahriman.web.schemas import LogSchema, LogsSchema, PackageNameSchema, PackageVersionSchema, RepositoryIdSchema
@ -40,8 +39,8 @@ class LogsView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/logs"]
@apidocs(
@ -97,7 +96,7 @@ class LogsView(StatusViewGuard, BaseView):
response = {
"package_base": package_base,
"status": status.view(),
"logs": "\n".join(f"[{pretty_datetime(log_record.created)}] {log_record.message}" for log_record in logs)
"logs": "\n".join(f"[{pretty_datetime(created)}] {message}" for _, created, message in logs)
}
return json_response(response)
@ -123,10 +122,14 @@ class LogsView(StatusViewGuard, BaseView):
try:
data = await self.request.json()
log_record = LogRecord.from_json(package_base, data)
created = data["created"]
record = data["message"]
version = data["version"]
except Exception as ex:
raise HTTPBadRequest(reason=str(ex))
self.service().package_logs_add(log_record)
# either read from process identifier from payload or assign to the current process identifier
process_id = data.get("process_id", LogRecordId("", "").process_id)
self.service().package_logs_add(LogRecordId(package_base, version, process_id), created, record)
raise HTTPNoContent

View File

@ -18,7 +18,6 @@
# 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
@ -41,8 +40,8 @@ class PackageView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Read
ROUTES = ["/api/v1/packages/{package}"]
@apidocs(
@ -106,7 +105,7 @@ class PackageView(StatusViewGuard, BaseView):
@apidocs(
tags=["Packages"],
summary="Update package",
description="Update package status and set its descriptor optionally",
description="Update package status and set its descriptior optionally",
permission=POST_PERMISSION,
error_400_enabled=True,
error_404_description="Repository is unknown",

View File

@ -21,7 +21,6 @@ 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
@ -41,8 +40,8 @@ class PackagesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION = UserAccess.Read
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages"]
@apidocs(

View File

@ -18,7 +18,6 @@
# 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
@ -36,8 +35,8 @@ class PatchView(StatusViewGuard, BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self
"""
DELETE_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
DELETE_PERMISSION = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/patches/{patch}"]
@apidocs(

View File

@ -18,7 +18,6 @@
# 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
@ -37,8 +36,8 @@ class PatchesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/patches"]
@apidocs(

View File

@ -18,7 +18,6 @@
# 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
@ -35,7 +34,7 @@ class AddView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/add"]
@apidocs(

View File

@ -18,7 +18,6 @@
# 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
@ -34,7 +33,7 @@ class LogsView(BaseView):
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
"""
DELETE_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
DELETE_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/logs"]
@apidocs(

View File

@ -18,7 +18,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
from typing import ClassVar
from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs
@ -35,8 +34,8 @@ class PGPView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self
"""
GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full
ROUTES = ["/api/v1/service/pgp"]
@apidocs(

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