Compare commits

..

13 Commits

Author SHA1 Message Date
3da3caaaa5 in-house bytes pkgbuild parser 2025-06-02 13:55:14 +03:00
9653fc4f4a type: support new configparser signatures 2025-05-31 02:16:07 +03:00
bcd46c66e8 test: use new-style fixtures instead of event_loop for asyncio 2025-05-12 15:57:05 +03:00
6ea56faede build: fix tox environment creation with the latest updates 2025-05-09 15:00:40 +03:00
9e346530f2 refactor: use backslashreplace error handling instead of guessing encoding 2025-05-08 14:03:47 +03:00
d283dccc1e type: update for new cors release 2025-03-17 13:56:59 +02:00
8a4e900ab9 docs: update docs
This commit includes following changes
* add newly added option to configuration referenec
* remove few legacy options from configuration schemas used for
  validation, which might lead to errors during validation.
  Note, however, that settings will be still read by the service
* add link to aurcache
* hide service-setup command description under spoiler
2025-03-17 13:43:04 +02:00
fa6cf8ce36 website: use date instead of version for listing logs
website: make dropdown from logs versions to add some space
2025-03-13 15:45:31 +02:00
a706fbb751 bug: handle dependencies iteratively (fix #141)
It has been found that if there are missing dependencies than whole
process will break instead of just skipping packages. During package
addition it is fine-ish, but it will break updates run
2025-03-13 15:45:27 +02:00
9a23f5c79d refactor: streamline migrations 2025-03-09 23:22:24 +02:00
aaab9069bf docs: rebuild indices 2025-03-09 15:43:41 +02:00
f00b575641 type: use ClassVar decorator for class attributes 2025-03-09 15:43:27 +02:00
6f57ed550b feat: refine log system (#142)
* refine package logging

* add interface

* revert version selection

* replace tuple with model

* rename column in logs table, add coverters

* generate process identifier for child proocesses
2025-03-09 14:46:33 +02:00
99 changed files with 960 additions and 600 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -80,7 +80,7 @@ Again, the most checks can be performed by `tox` command, though some additional
>>> clazz = Clazz() >>> clazz = Clazz()
""" """
CLAZZ_ATTRIBUTE = 42 CLAZZ_ATTRIBUTE: ClassVar[int] = 42
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
""" """
@ -96,6 +96,7 @@ Again, the most checks can be performed by `tox` command, though some additional
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated. * 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]`). * 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. * `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: * Recommended order of function definitions in class:
```python ```python

View File

@ -124,6 +124,14 @@ ahriman.core.database.migrations.m014\_auditlog module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 Module contents
--------------- ---------------

View File

@ -100,6 +100,14 @@ ahriman.models.log\_handler module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :show-inheritance:
ahriman.models.log\_record module
---------------------------------
.. automodule:: ahriman.models.log_record
:members:
:no-undoc-members:
:show-inheritance:
ahriman.models.log\_record\_id module ahriman.models.log\_record\_id module
------------------------------------- -------------------------------------

View File

@ -116,6 +116,14 @@ ahriman.web.schemas.login\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.web.schemas.logs\_schema module
--------------------------------------- ---------------------------------------
@ -292,14 +300,6 @@ ahriman.web.schemas.update\_flags\_schema module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.web.schemas.worker\_schema module
----------------------------------------- -----------------------------------------

View File

@ -12,6 +12,14 @@ ahriman.web.views.v1.service.add module
:no-undoc-members: :no-undoc-members:
:show-inheritance: :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 ahriman.web.views.v1.service.pgp module
--------------------------------------- ---------------------------------------

View File

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

View File

@ -81,6 +81,7 @@ 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. * ``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. * ``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. * ``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. * ``logging`` - path to logging configuration, string, required. Check ``logging.ini`` for reference.
``alpm:*`` groups ``alpm:*`` groups
@ -217,7 +218,7 @@ Mirrorlist generator plugin
``remote-pull`` group ``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.: 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,6 +56,13 @@ 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. 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 How to check service logs
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^

128
docs/requirements.txt Normal file
View File

@ -0,0 +1,128 @@
# 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,17 +12,20 @@ Initial setup
sudo ahriman -a x86_64 -r aur service-setup ... sudo ahriman -a x86_64 -r aur service-setup ...
``service-setup`` literally does the following steps: .. admonition:: Details
:collapsible: closed
#. ``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``):
.. code-block:: shell #.
Create ``/var/lib/ahriman/.makepkg.conf`` with ``makepkg.conf`` overrides if required (at least you might want to set ``PACKAGER``):
echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf .. code-block:: shell
#. 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``): Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``):
@ -67,7 +70,7 @@ Initial setup
echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman echo 'ahriman ALL=(ALL) NOPASSWD:SETENV: CARCHBUILD_CMD' | tee -a /etc/sudoers.d/ahriman
chmod 400 /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``: Start and enable ``ahriman@.timer`` via ``systemctl``:

View File

@ -60,10 +60,13 @@
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<div id="package-info-logs" class="tab-pane fade show active" role="tabpanel" aria-labelledby="package-info-logs-button" tabindex="0"> <div id="package-info-logs" class="tab-pane fade show active" role="tabpanel" aria-labelledby="package-info-logs-button" tabindex="0">
<div class="row"> <div class="row">
<div class="col-2"> <div class="col-1 dropend">
<nav id="package-info-logs-versions" class="nav flex-column"></nav> <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>
<div class="col-10"> <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> <pre class="language-console"><code id="package-info-logs-input" class="pre-scrollable language-console"></code><button id="package-info-logs-copy-button" type="button" class="btn language-console" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre>
</div> </div>
</div> </div>
@ -309,9 +312,9 @@
) )
.map(version => { .map(version => {
const link = document.createElement("a"); const link = document.createElement("a");
link.classList.add("nav-link"); link.classList.add("dropdown-item");
link.textContent = version.version; link.textContent = new Date(1000 * version.created).toISOStringShort();
link.href = "#"; link.href = "#";
link.onclick = _ => { link.onclick = _ => {
const logs = data const logs = data

View File

@ -27,10 +27,4 @@
top: 0; top: 0;
right: 5px; right: 5px;
} }
.nav-link.active {
pointer-events: none;
cursor: default;
color: black !important;
}
</style> </style>

View File

@ -25,15 +25,64 @@ dependencies = [
dynamic = ["version"] 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] [project.urls]
Documentation = "https://ahriman.readthedocs.io/" Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman" Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases" Changelog = "https://github.com/arcan1s/ahriman/releases"
[project.scripts] [dependency-groups]
ahriman = "ahriman.application.ahriman:run"
[project.optional-dependencies]
check = [ check = [
"autopep8", "autopep8",
"bandit", "bandit",
@ -47,24 +96,6 @@ docs = [
"shtab", "shtab",
"sphinx-argparse", "sphinx-argparse",
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734 "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 = [ tests = [
"pytest", "pytest",
@ -75,22 +106,6 @@ tests = [
"pytest-resource-path", "pytest-resource-path",
"pytest-spec", "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] [tool.flit.sdist]
include = [ include = [

View File

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

View File

@ -22,7 +22,7 @@ import logging
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from multiprocessing import Pool from multiprocessing import Pool
from typing import TypeVar from typing import ClassVar, TypeVar
from ahriman.application.lock import Lock from ahriman.application.lock import Lock
from ahriman.core.configuration import Configuration 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 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.:: must not be called directly. The recommended way is to call :func:`execute()` class method, e.g.::
>>> from ahriman.application.handlers import Add >>> from ahriman.application.handlers.add import Add
>>> >>>
>>> Add.execute(args) >>> Add.execute(args)
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = True ALLOW_MULTI_ARCHITECTURE_RUN: ClassVar[bool] = True
arguments: list[Callable[[SubParserAction], argparse.ArgumentParser]] arguments: ClassVar[list[Callable[[SubParserAction], argparse.ArgumentParser]]]
@classmethod @classmethod
def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool: def call(cls, args: argparse.Namespace, repository_id: RepositoryId) -> bool:

View File

@ -21,6 +21,7 @@ import argparse
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from dataclasses import fields from dataclasses import fields
from typing import ClassVar
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
from ahriman.core.alpm.remote import AUR, Official from ahriman.core.alpm.remote import AUR, Official
@ -40,7 +41,7 @@ class Search(Handler):
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
SORT_FIELDS = { SORT_FIELDS: ClassVar[set[str]] = {
field.name field.name
for field in fields(AURPackage) for field in fields(AURPackage)
if field.default_factory is not list if field.default_factory is not list

View File

@ -21,6 +21,7 @@ import argparse
from pathlib import Path from pathlib import Path
from pwd import getpwuid from pwd import getpwuid
from typing import ClassVar
from urllib.parse import quote_plus as url_encode from urllib.parse import quote_plus as url_encode
from ahriman.application.application import Application from ahriman.application.application import Application
@ -46,9 +47,9 @@ class Setup(Handler):
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
ARCHBUILD_COMMAND_PATH = Path("/") / "usr" / "bin" / "archbuild" ARCHBUILD_COMMAND_PATH: ClassVar[Path] = Path("/") / "usr" / "bin" / "archbuild"
MIRRORLIST_PATH = Path("/") / "etc" / "pacman.d" / "mirrorlist" MIRRORLIST_PATH: ClassVar[Path] = Path("/") / "etc" / "pacman.d" / "mirrorlist"
SUDOERS_DIR_PATH = Path("/") / "etc" / "sudoers.d" SUDOERS_DIR_PATH: ClassVar[Path] = Path("/") / "etc" / "sudoers.d"
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -23,6 +23,7 @@ import sys
from collections.abc import Generator from collections.abc import Generator
from importlib import metadata from importlib import metadata
from typing import ClassVar
from ahriman import __version__ from ahriman import __version__
from ahriman.application.handlers.handler import Handler, SubParserAction from ahriman.application.handlers.handler import Handler, SubParserAction
@ -36,11 +37,11 @@ class Versions(Handler):
version handler version handler
Attributes: Attributes:
PEP423_PACKAGE_NAME(str): (class attribute) special regex for valid PEP423 package name PEP423_PACKAGE_NAME(re.Pattern[str]): (class attribute) special regex for valid PEP423 package name
""" """
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
PEP423_PACKAGE_NAME = re.compile(r"^[A-Za-z0-9._-]+") PEP423_PACKAGE_NAME: ClassVar[re.Pattern[str]] = re.compile(r"^[A-Za-z0-9._-]+")
@classmethod @classmethod
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *, def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,

View File

@ -0,0 +1,183 @@
#
# 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 __future__ import annotations
from collections import defaultdict
from collections.abc import Iterator
from dataclasses import dataclass
from enum import ReprEnum
from types import SimpleNamespace
from typing import Generator, IO, Self
from ahriman.models.pkgbuild_patch import PkgbuildPatch
class PkgbuildToken(bytes, ReprEnum):
Comment = b"#"
Assignment = b"="
SingleQuote = b"'"
DoubleQuote = b"\""
Space = b" "
NewLine = b"\n"
ParenthesisOpen = b"("
ParenthesisClose = b")"
FunctionStarts = b"function"
FunctionDeclaration = b"()"
BraceOpen = b"{"
BraceClose = b"}"
@dataclass
class PkgbuildWord:
word: bytes
quote: bytes | None
@property
def closing(self) -> PkgbuildToken | None:
if self.quote:
return None
match self.word:
case PkgbuildToken.ParenthesisOpen:
return PkgbuildToken.ParenthesisClose
case PkgbuildToken.BraceOpen:
return PkgbuildToken.BraceClose
return None
@property
def original(self) -> bytes:
quote = self.quote or b""
return quote + self.word + quote
def __bool__(self) -> bool:
return bool(self.original)
class BytesPkgbuildParser(Iterator[PkgbuildPatch]):
def __init__(self, stream: IO[bytes]) -> None:
self._io = stream
def _next(self, *, declaration: bool) -> bytes:
while not (token := self._next_token(declaration=declaration)):
continue
return token
def _next_token(self, *, declaration: bool) -> bytes:
buffer = b""
while word := self._next_word():
match word:
case PkgbuildWord(PkgbuildToken.Comment, None):
self._io.readline()
case PkgbuildWord(PkgbuildToken.NewLine, None):
if declaration:
buffer = b""
return buffer
case PkgbuildWord(PkgbuildToken.Assignment, None) if declaration:
return buffer
case PkgbuildWord(PkgbuildToken.Space, None) if declaration:
if buffer.endswith(PkgbuildToken.FunctionDeclaration):
return buffer
buffer = b""
continue
case PkgbuildWord(PkgbuildToken.Space, None):
return buffer
case PkgbuildWord(PkgbuildToken.ParenthesisOpen, None):
buffer += PkgbuildToken.ParenthesisOpen
buffer += b"".join(self._next_words_until(PkgbuildWord(PkgbuildToken.ParenthesisClose, None)))
case PkgbuildWord(PkgbuildToken.BraceOpen, None):
buffer += PkgbuildToken.BraceOpen
buffer += b"".join(self._next_words_until(PkgbuildWord(PkgbuildToken.BraceClose, None)))
case PkgbuildWord(token, _):
buffer += token
raise StopIteration
def _next_word(self) -> PkgbuildWord:
# pass SimpleNamespace as an argument to implement side effects
def generator(quote: SimpleNamespace) -> Generator[bytes, None, None]:
while token := self._io.read(1):
match token:
case (PkgbuildToken.SingleQuote | PkgbuildToken.DoubleQuote) if quote.open is None:
quote.open = token
case closing_quote if closing_quote == quote.open:
return
case part:
yield part
if quote.open is None:
return
if quote.open is not None:
raise ValueError("No closing quotation")
open_quote = SimpleNamespace(open=None)
value = b"".join(generator(open_quote))
return PkgbuildWord(value, open_quote.open)
def _next_words_until(self, ending: PkgbuildWord) -> Generator[bytes, None, None]:
braces = defaultdict(int)
while element := self._next_word():
yield element.original
match element:
case PkgbuildWord(token, None) if braces[token] > 0:
braces[token] -= 1
case with_closure if (closing := with_closure.closing) is not None:
braces[closing] += 1
case _ if element == ending:
return
if any(brace for brace in braces.values() if brace > 0):
raise ValueError("Unclosed parenthesis and/or braces found")
raise ValueError(f"No matching ending element {ending.word} found")
def parse(self) -> Generator[PkgbuildPatch, None, None]:
"""
parse source stream and yield parsed entries
Yields:
PkgbuildPatch: extracted a PKGBUILD node
"""
yield from self
def __iter__(self) -> Self:
"""
base iterator method
Returns:
Self: iterator instance
"""
return self
def __next__(self) -> PkgbuildPatch:
key = self._next(declaration=True)
value = self._next(declaration=False)
return PkgbuildPatch(key.decode(encoding="utf8"), value.decode(encoding="utf8"))

View File

@ -23,6 +23,7 @@ import shutil
from email.utils import parsedate_to_datetime from email.utils import parsedate_to_datetime
from pathlib import Path from pathlib import Path
from pyalpm import DB # type: ignore[import-not-found] from pyalpm import DB # type: ignore[import-not-found]
from typing import ClassVar
from urllib.parse import urlparse from urllib.parse import urlparse
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -41,7 +42,7 @@ class PacmanDatabase(SyncHttpClient):
sync_files_database(bool): sync files database sync_files_database(bool): sync files database
""" """
LAST_MODIFIED_HEADER = "Last-Modified" LAST_MODIFIED_HEADER: ClassVar[str] = "Last-Modified"
def __init__(self, database: DB, configuration: Configuration) -> None: def __init__(self, database: DB, configuration: Configuration) -> None:
""" """

View File

@ -34,14 +34,14 @@ class PkgbuildToken(StrEnum):
well-known tokens dictionary well-known tokens dictionary
Attributes: Attributes:
ArrayEnds(PkgbuildToken): (class attribute) array ends token ArrayEnds(PkgbuildToken): array ends token
ArrayStarts(PkgbuildToken): (class attribute) array starts token ArrayStarts(PkgbuildToken): array starts token
Comma(PkgbuildToken): (class attribute) comma token Comma(PkgbuildToken): comma token
Comment(PkgbuildToken): (class attribute) comment token Comment(PkgbuildToken): comment token
FunctionDeclaration(PkgbuildToken): (class attribute) function declaration token FunctionDeclaration(PkgbuildToken): function declaration token
FunctionEnds(PkgbuildToken): (class attribute) function ends token FunctionEnds(PkgbuildToken): function ends token
FunctionStarts(PkgbuildToken): (class attribute) function starts token FunctionStarts(PkgbuildToken): function starts token
NewLine(PkgbuildToken): (class attribute) new line token NewLine(PkgbuildToken): new line token
""" """
ArrayStarts = "(" ArrayStarts = "("

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import Any from typing import Any, ClassVar
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote.remote import Remote 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_RPC_VERSION(str): (class attribute) default AUR RPC version
""" """
DEFAULT_AUR_URL = "https://aur.archlinux.org" DEFAULT_AUR_URL: ClassVar[str] = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc" DEFAULT_RPC_URL: ClassVar[str] = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5" DEFAULT_RPC_VERSION: ClassVar[str] = "5"
@classmethod @classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str: 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 # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import Any from typing import Any, ClassVar
from ahriman.core.alpm.pacman import Pacman from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote.remote import Remote 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_RPC_URL(str): (class attribute) default archlinux repositories RPC url
""" """
DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org" DEFAULT_ARCHLINUX_GIT_URL: ClassVar[str] = "https://gitlab.archlinux.org"
DEFAULT_ARCHLINUX_URL = "https://archlinux.org" DEFAULT_ARCHLINUX_URL: ClassVar[str] = "https://archlinux.org"
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"] DEFAULT_SEARCH_REPOSITORIES: ClassVar[list[str]] = ["Core", "Extra", "Multilib"]
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json" DEFAULT_RPC_URL: ClassVar[str] = "https://archlinux.org/packages/search/json"
@classmethod @classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str: def remote_git_url(cls, package_base: str, repository: str) -> str:

View File

@ -21,6 +21,7 @@ import shutil
from collections.abc import Generator from collections.abc import Generator
from pathlib import Path from pathlib import Path
from typing import ClassVar
from ahriman.core.exceptions import CalledProcessError from ahriman.core.exceptions import CalledProcessError
from ahriman.core.log import LazyLogging from ahriman.core.log import LazyLogging
@ -42,9 +43,9 @@ class Sources(LazyLogging):
GITCONFIG(dict[str, str]): (class attribute) git config options to suppress annoying hints GITCONFIG(dict[str, str]): (class attribute) git config options to suppress annoying hints
""" """
DEFAULT_BRANCH = "master" # default fallback branch DEFAULT_BRANCH: ClassVar[str] = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost") DEFAULT_COMMIT_AUTHOR: ClassVar[tuple[str, str]] = ("ahriman", "ahriman@localhost")
GITCONFIG = { GITCONFIG: ClassVar[dict[str, str]] = {
"init.defaultBranch": DEFAULT_BRANCH, "init.defaultBranch": DEFAULT_BRANCH,
} }

View File

@ -22,7 +22,7 @@ import shlex
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any, Self from typing import Any, ClassVar, Self
from ahriman.core.configuration.configuration_multi_dict import ConfigurationMultiDict from ahriman.core.configuration.configuration_multi_dict import ConfigurationMultiDict
from ahriman.core.configuration.shell_interpolator import ShellInterpolator from ahriman.core.configuration.shell_interpolator import ShellInterpolator
@ -65,8 +65,8 @@ class Configuration(configparser.RawConfigParser):
""" """
_LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS = ["web"] _LEGACY_ARCHITECTURE_SPECIFIC_SECTIONS = ["web"]
ARCHITECTURE_SPECIFIC_SECTIONS = ["alpm", "build", "sign"] ARCHITECTURE_SPECIFIC_SECTIONS: ClassVar[list[str]] = ["alpm", "build", "sign"]
SYSTEM_CONFIGURATION_PATH = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini" SYSTEM_CONFIGURATION_PATH: ClassVar[Path] = Path(sys.prefix) / "share" / "ahriman" / "settings" / "ahriman.ini"
def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None: def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None:
""" """

View File

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

View File

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

View File

@ -28,9 +28,6 @@ class ShellTemplate(Template):
""" """
extension to the default :class:`Template` class, which also adds additional tokens to braced regex and enables extension to the default :class:`Template` class, which also adds additional tokens to braced regex and enables
bash expansion bash expansion
Attributes:
braceidpattern(str): regular expression to match every character except for closing bracket
""" """
braceidpattern = r"(?a:[_a-z0-9][^}]*)" braceidpattern = r"(?a:[_a-z0-9][^}]*)"

View File

@ -62,24 +62,31 @@ class Migrations(LazyLogging):
""" """
return Migrations(connection, configuration).run() return Migrations(connection, configuration).run()
def migration(self, cursor: Cursor, migration: Migration) -> None: def apply_migrations(self, migrations: list[Migration]) -> None:
""" """
perform single migration perform migrations explicitly
Args: Args:
cursor(Cursor): connection cursor migrations(list[Migration]): list of migrations to perform
migration(Migration): single migration to perform
""" """
self.logger.info("applying table migration %s at index %s", migration.name, migration.index) previous_isolation = self.connection.isolation_level
for statement in migration.steps: try:
cursor.execute(statement) self.connection.isolation_level = None
self.logger.info("table migration %s at index %s has been applied", migration.name, migration.index) cursor = self.connection.cursor()
try:
self.logger.info("perform data migration %s at index %s", migration.name, migration.index) cursor.execute("begin exclusive")
migration.migrate_data(self.connection, self.configuration) for migration in migrations:
self.logger.info( self.perform_migration(cursor, migration)
"data migration %s at index %s has been performed", except Exception:
migration.name, migration.index) self.logger.exception("migration failed with exception")
cursor.execute("rollback")
raise
else:
cursor.execute("commit")
finally:
cursor.close()
finally:
self.connection.isolation_level = previous_isolation
def migrations(self) -> list[Migration]: def migrations(self) -> list[Migration]:
""" """
@ -114,6 +121,25 @@ class Migrations(LazyLogging):
return migrations 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: def run(self) -> MigrationResult:
""" """
perform migrations perform migrations
@ -122,6 +148,7 @@ class Migrations(LazyLogging):
MigrationResult: current schema version MigrationResult: current schema version
""" """
migrations = self.migrations() migrations = self.migrations()
current_version = self.user_version() current_version = self.user_version()
expected_version = len(migrations) expected_version = len(migrations)
result = MigrationResult(old_version=current_version, new_version=expected_version) result = MigrationResult(old_version=current_version, new_version=expected_version)
@ -130,25 +157,8 @@ class Migrations(LazyLogging):
self.logger.info("no migrations required") self.logger.info("no migrations required")
return result return result
previous_isolation = self.connection.isolation_level self.apply_migrations(migrations[current_version:])
try: self.connection.execute(f"pragma user_version = {expected_version}") # no support for ? placeholders
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) self.logger.info("migrations have been performed from version %s to %s", result.old_version, result.new_version)
return result return result

View File

@ -22,7 +22,6 @@ import contextlib
from functools import cached_property from functools import cached_property
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.configuration.schema import ConfigurationSchema
from ahriman.core.status.web_client import WebClient from ahriman.core.status.web_client import WebClient
from ahriman.core.triggers import Trigger from ahriman.core.triggers import Trigger
from ahriman.models.repository_id import RepositoryId from ahriman.models.repository_id import RepositoryId
@ -34,7 +33,7 @@ class DistributedSystem(Trigger, WebClient):
simple class to (un)register itself as a distributed worker simple class to (un)register itself as a distributed worker
""" """
CONFIGURATION_SCHEMA: ConfigurationSchema = { CONFIGURATION_SCHEMA = {
"worker": { "worker": {
"type": "dict", "type": "dict",
"schema": { "schema": {

View File

@ -95,19 +95,6 @@ class DuplicateRunError(RuntimeError):
self, "Another application instance is run. This error can be suppressed by using --force flag.") self, "Another application instance is run. This error can be suppressed by using --force flag.")
class EncodeError(ValueError):
"""
exception used for bytes encoding errors
"""
def __init__(self, encodings: list[str]) -> None:
"""
Args:
encodings(list[str]): list of encodings tried
"""
ValueError.__init__(self, f"Could not encode bytes by using {encodings}")
class ExitCode(RuntimeError): class ExitCode(RuntimeError):
""" """
special exception which has to be thrown to return non-zero status without error message special exception which has to be thrown to return non-zero status without error message

View File

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

View File

@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import ClassVar
from ahriman.core.formatters.string_printer import StringPrinter from ahriman.core.formatters.string_printer import StringPrinter
from ahriman.models.property import Property from ahriman.models.property import Property
@ -26,10 +28,11 @@ class PackageStatsPrinter(StringPrinter):
print packages statistics print packages statistics
Attributes: Attributes:
MAX_COUNT(int): (class attribute) maximum number of packages to print
events(dict[str, int]): map of package to its event frequency events(dict[str, int]): map of package to its event frequency
""" """
MAX_COUNT = 10 MAX_COUNT: ClassVar[int] = 10
def __init__(self, events: dict[str, int]) -> None: def __init__(self, events: dict[str, int]) -> None:
""" """

View File

@ -21,6 +21,7 @@ import logging
from logging.config import fileConfig from logging.config import fileConfig
from pathlib import Path from pathlib import Path
from typing import ClassVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.log.http_log_handler import HttpLogHandler from ahriman.core.log.http_log_handler import HttpLogHandler
@ -38,9 +39,9 @@ class LogLoader:
DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device DEFAULT_SYSLOG_DEVICE(Path): (class attribute) default path to syslog device
""" """
DEFAULT_LOG_FORMAT = "[%(levelname)s %(asctime)s] [%(filename)s:%(lineno)d %(funcName)s]: %(message)s" DEFAULT_LOG_FORMAT: ClassVar[str] = "[%(levelname)s %(asctime)s] [%(name)s]: %(message)s"
DEFAULT_LOG_LEVEL = logging.DEBUG DEFAULT_LOG_LEVEL: ClassVar[int] = logging.DEBUG
DEFAULT_SYSLOG_DEVICE = Path("/") / "dev" / "log" DEFAULT_SYSLOG_DEVICE: ClassVar[Path] = Path("/") / "dev" / "log"
@staticmethod @staticmethod
def handler(selected: LogHandler | None) -> LogHandler: def handler(selected: LogHandler | None) -> LogHandler:

View File

@ -67,14 +67,6 @@ class ReportTrigger(Trigger):
"type": "string", "type": "string",
"allowed": ["email"], "allowed": ["email"],
}, },
"full_template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template_full"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"homepage": { "homepage": {
"type": "string", "type": "string",
"empty": False, "empty": False,
@ -132,26 +124,16 @@ class ReportTrigger(Trigger):
}, },
"template": { "template": {
"type": "string", "type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"], "dependencies": ["templates"],
"required": True, "required": True,
"empty": False, "empty": False,
}, },
"template_full": { "template_full": {
"type": "string", "type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"], "dependencies": ["templates"],
"required": True, "required": True,
"empty": False, "empty": False,
}, },
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"templates": { "templates": {
"type": "list", "type": "list",
"coerce": "list", "coerce": "list",
@ -199,19 +181,10 @@ class ReportTrigger(Trigger):
}, },
"template": { "template": {
"type": "string", "type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"], "dependencies": ["templates"],
"required": True, "required": True,
"empty": False, "empty": False,
}, },
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"templates": { "templates": {
"type": "list", "type": "list",
"coerce": "list", "coerce": "list",
@ -225,76 +198,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",
"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": { "remote-call": {
"type": "dict", "type": "dict",
"schema": { "schema": {
@ -354,19 +257,10 @@ class ReportTrigger(Trigger):
}, },
"template": { "template": {
"type": "string", "type": "string",
"excludes": ["template_path"],
"dependencies": ["templates"], "dependencies": ["templates"],
"required": True, "required": True,
"empty": False, "empty": False,
}, },
"template_path": {
"type": "path",
"coerce": "absolute_path",
"excludes": ["template"],
"required": True,
"path_exists": True,
"path_type": "file",
},
"templates": { "templates": {
"type": "list", "type": "list",
"coerce": "list", "coerce": "list",
@ -380,6 +274,67 @@ 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: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:

View File

@ -17,6 +17,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from typing import ClassVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient from ahriman.core.http import SyncHttpClient
from ahriman.core.report.jinja_template import JinjaTemplate from ahriman.core.report.jinja_template import JinjaTemplate
@ -39,8 +41,8 @@ class Telegram(Report, JinjaTemplate, SyncHttpClient):
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
""" """
TELEGRAM_API_URL = "https://api.telegram.org" TELEGRAM_API_URL: ClassVar[str] = "https://api.telegram.org"
TELEGRAM_MAX_CONTENT_LENGTH = 4096 TELEGRAM_MAX_CONTENT_LENGTH: ClassVar[int] = 4096
def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration, section: str) -> None:
""" """

View File

@ -22,6 +22,7 @@ import itertools
from collections.abc import Callable, Generator from collections.abc import Callable, Generator
from pathlib import Path from pathlib import Path
from typing import ClassVar
from ahriman.core.utils import utcnow from ahriman.core.utils import utcnow
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -35,7 +36,7 @@ class PkgbuildGenerator:
PKGBUILD_STATIC_PROPERTIES(list[PkgbuildPatch]): (class attribute) list of default pkgbuild static properties PKGBUILD_STATIC_PROPERTIES(list[PkgbuildPatch]): (class attribute) list of default pkgbuild static properties
""" """
PKGBUILD_STATIC_PROPERTIES = [ PKGBUILD_STATIC_PROPERTIES: ClassVar[list[PkgbuildPatch]] = [
PkgbuildPatch("pkgrel", "1"), PkgbuildPatch("pkgrel", "1"),
PkgbuildPatch("arch", ["any"]), PkgbuildPatch("arch", ["any"]),
] ]

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from collections.abc import Callable from collections.abc import Callable
from typing import ClassVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.configuration.schema import ConfigurationSchema from ahriman.core.configuration.schema import ConfigurationSchema
@ -56,8 +57,8 @@ class Trigger(LazyLogging):
>>> loader.on_result(Result(), []) >>> loader.on_result(Result(), [])
""" """
CONFIGURATION_SCHEMA: ConfigurationSchema = {} CONFIGURATION_SCHEMA: ClassVar[ConfigurationSchema] = {}
CONFIGURATION_SCHEMA_FALLBACK: str | None = None CONFIGURATION_SCHEMA_FALLBACK: ClassVar[str | None] = None
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None: def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
""" """

View File

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

View File

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

View File

@ -27,10 +27,10 @@ class AuthSettings(StrEnum):
web authorization type web authorization type
Attributes: Attributes:
Disabled(AuthSettings): (class attribute) authorization is disabled Disabled(AuthSettings): authorization is disabled
Configuration(AuthSettings): (class attribute) configuration based authorization Configuration(AuthSettings): configuration based authorization
OAuth(AuthSettings): (class attribute) OAuth based provider OAuth(AuthSettings): OAuth based provider
PAM(AuthSettings): (class attribute) PAM based provider PAM(AuthSettings): PAM based provider
""" """
Disabled = "disabled" Disabled = "disabled"

View File

@ -29,11 +29,11 @@ class BuildStatusEnum(StrEnum):
build status enumeration build status enumeration
Attributes: Attributes:
Unknown(BuildStatusEnum): (class attribute) build status is unknown Unknown(BuildStatusEnum): build status is unknown
Pending(BuildStatusEnum): (class attribute) package is out-of-dated and will be built soon Pending(BuildStatusEnum): package is out-of-dated and will be built soon
Building(BuildStatusEnum): (class attribute) package is building right now Building(BuildStatusEnum): package is building right now
Failed(BuildStatusEnum): (class attribute) package build failed Failed(BuildStatusEnum): package build failed
Success(BuildStatusEnum): (class attribute) package has been built without errors Success(BuildStatusEnum): package has been built without errors
""" """
Unknown = "unknown" Unknown = "unknown"

View File

@ -28,10 +28,10 @@ class EventType(StrEnum):
predefined event types predefined event types
Attributes: Attributes:
PackageOutdated(EventType): (class attribute) package has been marked as out-of-date PackageOutdated(EventType): package has been marked as out-of-date
PackageRemoved(EventType): (class attribute) package has been removed PackageRemoved(EventType): package has been removed
PackageUpdateFailed(EventType): (class attribute) package update has been failed PackageUpdateFailed(EventType): package update has been failed
PackageUpdated(EventType): (class attribute) package has been updated PackageUpdated(EventType): package has been updated
""" """
PackageOutdated = "package-outdated" PackageOutdated = "package-outdated"

View File

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

View File

@ -20,7 +20,7 @@
import uuid import uuid
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, ClassVar from typing import ClassVar
@dataclass(frozen=True) @dataclass(frozen=True)
@ -29,39 +29,21 @@ class LogRecordId:
log record process identifier log record process identifier
Attributes: Attributes:
DEFAULT_PROCESS_ID(str): (class attribute) default process identifier
package_base(str): package base for which log record belongs package_base(str): package base for which log record belongs
version(str): package version for which log record belongs version(str): package version for which log record belongs
_process_id(str | None, optional): unique process identifier process_id(str, optional): unique process identifier
""" """
package_base: str package_base: str
version: str version: str
_process_id: str | None = None process_id: str = ""
DEFAULT_PROCESS_ID: ClassVar[str] = str(uuid.uuid4()) DEFAULT_PROCESS_ID: ClassVar[str] = str(uuid.uuid4())
@property def __post_init__(self) -> None:
def process_id(self) -> str:
""" """
unique process identifier assign process identifier from default if not set
Returns:
str: process identifier as set
""" """
return self._process_id or self.DEFAULT_PROCESS_ID if not self.process_id:
object.__setattr__(self, "process_id", self.DEFAULT_PROCESS_ID)
def __eq__(self, other: Any) -> bool:
"""
check if other is the same object
Args:
other(Any): other object instance
Returns:
bool: ``True`` if the other object is the same and ``False`` otherwise
"""
if not isinstance(other, LogRecordId):
return False
return self.package_base == other.package_base \
and self.version == other.version \
and self.process_id == other.process_id

View File

@ -32,13 +32,13 @@ class PackageSource(StrEnum):
package source for addition enumeration package source for addition enumeration
Attributes: Attributes:
Auto(PackageSource): (class attribute) automatically determine type of the source Auto(PackageSource): automatically determine type of the source
Archive(PackageSource): (class attribute) source is a package archive Archive(PackageSource): source is a package archive
AUR(PackageSource): (class attribute) source is an AUR package for which it should search AUR(PackageSource): source is an AUR package for which it should search
Directory(PackageSource): (class attribute) source is a directory which contains packages Directory(PackageSource): source is a directory which contains packages
Local(PackageSource): (class attribute) source is locally stored PKGBUILD Local(PackageSource): source is locally stored PKGBUILD
Remote(PackageSource): (class attribute) source is remote (http, ftp etc...) link Remote(PackageSource): source is remote (http, ftp etc...) link
Repository(PackageSource): (class attribute) source is official repository Repository(PackageSource): source is official repository
Examples: Examples:
In case if source is unknown the :func:`resolve()` and the source 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 pacman database synchronization flag
Attributes: Attributes:
Disabled(PacmanSynchronization): (class attribute) do not synchronize local database Disabled(PacmanSynchronization): do not synchronize local database
Enabled(PacmanSynchronization): (class attribute) synchronize local database (same as pacman -Sy) Enabled(PacmanSynchronization): synchronize local database (same as pacman -Sy)
Force(PacmanSynchronization): (class attribute) force synchronize local database (same as pacman -Syy) Force(PacmanSynchronization): force synchronize local database (same as pacman -Syy)
""" """
Disabled = 0 Disabled = 0

View File

@ -21,10 +21,9 @@ from collections.abc import Iterator, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from io import StringIO from io import StringIO
from pathlib import Path from pathlib import Path
from typing import Any, IO, Self from typing import Any, ClassVar, IO, Self
from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken from ahriman.core.alpm.pkgbuild_parser import PkgbuildParser, PkgbuildToken
from ahriman.core.exceptions import EncodeError
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -34,13 +33,13 @@ class Pkgbuild(Mapping[str, Any]):
model and proxy for PKGBUILD properties model and proxy for PKGBUILD properties
Attributes: Attributes:
DEFAULT_ENCODINGS(list[str]): (class attribute) list of encoding to be applied on the file content DEFAULT_ENCODINGS(str): (class attribute) default encoding to be applied on the file content
fields(dict[str, PkgbuildPatch]): PKGBUILD fields fields(dict[str, PkgbuildPatch]): PKGBUILD fields
""" """
fields: dict[str, PkgbuildPatch] fields: dict[str, PkgbuildPatch]
DEFAULT_ENCODINGS = ["utf8", "latin-1"] DEFAULT_ENCODINGS: ClassVar[str] = "utf8"
@property @property
def variables(self) -> dict[str, str]: def variables(self) -> dict[str, str]:
@ -58,13 +57,13 @@ class Pkgbuild(Mapping[str, Any]):
} }
@classmethod @classmethod
def from_file(cls, path: Path, encodings: list[str] | None = None) -> Self: def from_file(cls, path: Path, encoding: str | None = None) -> Self:
""" """
parse PKGBUILD from the file parse PKGBUILD from the file
Args: Args:
path(Path): path to the PKGBUILD file path(Path): path to the PKGBUILD file
encodings(list[str] | None, optional): the encoding of the file (Default value = None) encoding(str | None, optional): the encoding of the file (Default value = None)
Returns: Returns:
Self: constructed instance of self Self: constructed instance of self
@ -77,15 +76,10 @@ class Pkgbuild(Mapping[str, Any]):
content = input_file.read() content = input_file.read()
# decode bytes content based on either # decode bytes content based on either
encodings = encodings or cls.DEFAULT_ENCODINGS encoding = encoding or cls.DEFAULT_ENCODINGS
for encoding in encodings: io = StringIO(content.decode(encoding, errors="backslashreplace"))
try:
io = StringIO(content.decode(encoding))
return cls.from_io(io)
except ValueError:
pass
raise EncodeError(encodings) return cls.from_io(io)
@classmethod @classmethod
def from_io(cls, stream: IO[str]) -> Self: def from_io(cls, stream: IO[str]) -> Self:

View File

@ -27,13 +27,13 @@ class ReportSettings(StrEnum):
report targets enumeration report targets enumeration
Attributes: Attributes:
Disabled(ReportSettings): (class attribute) option which generates no report for testing purpose Disabled(ReportSettings): option which generates no report for testing purpose
HTML(ReportSettings): (class attribute) html report generation HTML(ReportSettings): html report generation
Email(ReportSettings): (class attribute) email report generation Email(ReportSettings): email report generation
Console(ReportSettings): (class attribute) print result to console Console(ReportSettings): print result to console
Telegram(ReportSettings): (class attribute) markdown report to telegram channel Telegram(ReportSettings): markdown report to telegram channel
RSS(ReportSettings): (class attribute) RSS report generation RSS(ReportSettings): RSS report generation
RemoteCall(ReportSettings): (class attribute) remote ahriman server call RemoteCall(ReportSettings): remote ahriman server call
""" """
Disabled = "disabled" # for testing purpose Disabled = "disabled" # for testing purpose

View File

@ -20,7 +20,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from typing import Any, Self from typing import Any, ClassVar, Self
from ahriman.models.package import Package 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(list[str]): (class attribute) list of statues according to their priorities
""" """
STATUS_PRIORITIES = [ STATUS_PRIORITIES: ClassVar[list[str]] = [
"failed", "failed",
"removed", "removed",
"updated", "updated",

View File

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

View File

@ -27,9 +27,9 @@ class SmtpSSLSettings(StrEnum):
SMTP SSL mode enumeration SMTP SSL mode enumeration
Attributes: Attributes:
Disabled(SmtpSSLSettings): (class attribute) no SSL enabled Disabled(SmtpSSLSettings): no SSL enabled
SSL(SmtpSSLSettings): (class attribute) use SMTP_SSL instead of normal SMTP client SSL(SmtpSSLSettings): use SMTP_SSL instead of normal SMTP client
STARTTLS(SmtpSSLSettings): (class attribute) use STARTTLS in normal SMTP client STARTTLS(SmtpSSLSettings): use STARTTLS in normal SMTP client
""" """
Disabled = "disabled" Disabled = "disabled"

View File

@ -27,11 +27,11 @@ class UploadSettings(StrEnum):
remote synchronization targets enumeration remote synchronization targets enumeration
Attributes: Attributes:
Disabled(UploadSettings): (class attribute) no sync will be performed, required for testing purpose Disabled(UploadSettings): no sync will be performed, required for testing purpose
Rsync(UploadSettings): (class attribute) sync via rsync Rsync(UploadSettings): sync via rsync
S3(UploadSettings): (class attribute) sync to Amazon S3 S3(UploadSettings): sync to Amazon S3
GitHub(UploadSettings): (class attribute) sync to GitHub releases page GitHub(UploadSettings): sync to GitHub releases page
RemoteService(UploadSettings): (class attribute) sync to another ahriman instance RemoteService(UploadSettings): sync to another ahriman instance
""" """
Disabled = "disabled" # for testing purpose Disabled = "disabled" # for testing purpose

View File

@ -21,7 +21,7 @@ import bcrypt
from dataclasses import dataclass, replace from dataclasses import dataclass, replace
from secrets import token_urlsafe as generate_password from secrets import token_urlsafe as generate_password
from typing import Self from typing import ClassVar, Self
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -69,7 +69,7 @@ class User:
packager_id: str | None = None packager_id: str | None = None
key: str | None = None key: str | None = None
SUPPORTED_ALGOS = {"$2$", "$2a$", "$2x$", "$2y$", "$2b$"} SUPPORTED_ALGOS: ClassVar[set[str]] = {"$2$", "$2a$", "$2x$", "$2y$", "$2b$"}
def __post_init__(self) -> None: def __post_init__(self) -> None:
""" """

View File

@ -27,11 +27,11 @@ class UserAccess(StrEnum):
web user access enumeration web user access enumeration
Attributes: Attributes:
Unauthorized(UserAccess): (class attribute) user can access specific resources which are marked as available Unauthorized(UserAccess): user can access specific resources which are marked as available
without authorization (e.g. login, logout, static) without authorization (e.g. login, logout, static)
Read(UserAccess): (class attribute) user can read the page Read(UserAccess): user can read the page
Reporter(UserAccess): (class attribute) user can read everything and is able to perform some modifications Reporter(UserAccess): user can read everything and is able to perform some modifications
Full(UserAccess): (class attribute) user has full access Full(UserAccess): user has full access
""" """
Unauthorized = "unauthorized" Unauthorized = "unauthorized"

View File

@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
import aiohttp_cors # type: ignore[import-untyped] import aiohttp_cors
from aiohttp.web import Application from aiohttp.web import Application
@ -36,7 +36,7 @@ def setup_cors(application: Application) -> aiohttp_cors.CorsConfig:
aiohttp_cors.CorsConfig: generated CORS configuration aiohttp_cors.CorsConfig: generated CORS configuration
""" """
cors = aiohttp_cors.setup(application, defaults={ cors = aiohttp_cors.setup(application, defaults={
"*": aiohttp_cors.ResourceOptions( "*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
expose_headers="*", expose_headers="*",
allow_headers="*", allow_headers="*",
allow_methods="*", allow_methods="*",

View File

@ -19,7 +19,7 @@
# #
import aiohttp_jinja2 import aiohttp_jinja2
from typing import Any from typing import Any, ClassVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -35,7 +35,7 @@ class DocsView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/api-docs"] ROUTES = ["/api-docs"]
@classmethod @classmethod

View File

@ -19,6 +19,7 @@
# #
from aiohttp.web import Response, json_response from aiohttp.web import Response, json_response
from collections.abc import Callable from collections.abc import Callable
from typing import ClassVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.utils import partition from ahriman.core.utils import partition
@ -35,7 +36,7 @@ class SwaggerView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/api-docs/swagger.json"] ROUTES = ["/api-docs/swagger.json"]
@classmethod @classmethod

View File

@ -18,9 +18,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
from aiohttp_cors import CorsViewMixin # type: ignore[import-untyped] from aiohttp_cors import CorsViewMixin
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import TypeVar from typing import ClassVar, TypeVar
from ahriman.core.auth import Auth from ahriman.core.auth import Auth
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
@ -46,8 +46,8 @@ class BaseView(View, CorsViewMixin):
ROUTES(list[str]): (class attribute) list of supported routes ROUTES(list[str]): (class attribute) list of supported routes
""" """
OPTIONS_PERMISSION = UserAccess.Unauthorized OPTIONS_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES: list[str] = [] ROUTES: ClassVar[list[str]] = []
@property @property
def configuration(self) -> Configuration: def configuration(self) -> Configuration:

View File

@ -19,7 +19,7 @@
# #
import aiohttp_jinja2 import aiohttp_jinja2
from typing import Any from typing import Any, ClassVar
from ahriman.core.auth.helpers import authorized_userid from ahriman.core.auth.helpers import authorized_userid
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -48,7 +48,7 @@ class IndexView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/", "/index.html"] ROUTES = ["/", "/index.html"]
@aiohttp_jinja2.template("build-status.jinja2") @aiohttp_jinja2.template("build-status.jinja2")

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPFound, HTTPNotFound from aiohttp.web import HTTPFound, HTTPNotFound
from typing import ClassVar
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.views.base import BaseView from ahriman.web.views.base import BaseView
@ -31,7 +32,7 @@ class StaticView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/favicon.ico"] ROUTES = ["/favicon.ico"]
async def get(self) -> None: async def get(self) -> None:

View File

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

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from typing import ClassVar
from ahriman.models.event import Event from ahriman.models.event import Event
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -35,7 +36,7 @@ class EventsView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = POST_PERMISSION = UserAccess.Full GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
ROUTES = ["/api/v1/events"] ROUTES = ["/api/v1/events"]
@apidocs( @apidocs(

View File

@ -19,6 +19,7 @@
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from collections.abc import Callable from collections.abc import Callable
from typing import ClassVar
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.models.worker import Worker from ahriman.models.worker import Worker
@ -37,7 +38,7 @@ class WorkersView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full DELETE_PERMISSION = GET_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
ROUTES = ["/api/v1/distributed"] ROUTES = ["/api/v1/distributed"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from typing import ClassVar
from ahriman.models.changes import Changes from ahriman.models.changes import Changes
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -36,8 +37,8 @@ class ChangesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/changes"] ROUTES = ["/api/v1/packages/{package}/changes"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from typing import ClassVar
from ahriman.models.dependencies import Dependencies from ahriman.models.dependencies import Dependencies
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -36,8 +37,8 @@ class DependenciesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/dependencies"] ROUTES = ["/api/v1/packages/{package}/dependencies"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from typing import ClassVar
from ahriman.core.exceptions import UnknownPackageError from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.utils import pretty_datetime from ahriman.core.utils import pretty_datetime
@ -39,8 +40,8 @@ class LogsView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/logs"] ROUTES = ["/api/v1/packages/{package}/logs"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, HTTPNotFound, Response, json_response
from typing import ClassVar
from ahriman.core.exceptions import UnknownPackageError from ahriman.core.exceptions import UnknownPackageError
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
@ -40,8 +41,8 @@ class PackageView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full DELETE_PERMISSION = POST_PERMISSION = UserAccess.Full # type: ClassVar[UserAccess]
GET_PERMISSION = UserAccess.Read GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
ROUTES = ["/api/v1/packages/{package}"] ROUTES = ["/api/v1/packages/{package}"]
@apidocs( @apidocs(

View File

@ -21,6 +21,7 @@ import itertools
from aiohttp.web import HTTPNoContent, Response, json_response from aiohttp.web import HTTPNoContent, Response, json_response
from collections.abc import Callable from collections.abc import Callable
from typing import ClassVar
from ahriman.models.build_status import BuildStatus from ahriman.models.build_status import BuildStatus
from ahriman.models.package import Package from ahriman.models.package import Package
@ -40,8 +41,8 @@ class PackagesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = UserAccess.Read GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
POST_PERMISSION = UserAccess.Full POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/packages"] ROUTES = ["/api/v1/packages"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response from aiohttp.web import HTTPNoContent, HTTPNotFound, Response, json_response
from typing import ClassVar
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs from ahriman.web.apispec.decorators import apidocs
@ -35,8 +36,8 @@ class PatchView(StatusViewGuard, BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
DELETE_PERMISSION = UserAccess.Full DELETE_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
ROUTES = ["/api/v1/packages/{package}/patches/{patch}"] ROUTES = ["/api/v1/packages/{package}/patches/{patch}"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from typing import ClassVar
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -36,8 +37,8 @@ class PatchesView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
POST_PERMISSION = UserAccess.Full POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/packages/{package}/patches"] ROUTES = ["/api/v1/packages/{package}/patches"]
@apidocs( @apidocs(

View File

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

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent from aiohttp.web import HTTPBadRequest, HTTPNoContent
from typing import ClassVar
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs from ahriman.web.apispec.decorators import apidocs
@ -33,7 +34,7 @@ class LogsView(BaseView):
DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self DELETE_PERMISSION(UserAccess): (class attribute) delete permissions of self
""" """
DELETE_PERMISSION = UserAccess.Full DELETE_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/service/logs"] ROUTES = ["/api/v1/service/logs"]
@apidocs( @apidocs(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
# #
from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNotFound, Response, json_response
from collections.abc import Callable from collections.abc import Callable
from typing import ClassVar
from ahriman.core.alpm.remote import AUR from ahriman.core.alpm.remote import AUR
from ahriman.models.aur_package import AURPackage from ahriman.models.aur_package import AURPackage
@ -36,7 +37,7 @@ class SearchView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
ROUTES = ["/api/v1/service/search"] ROUTES = ["/api/v1/service/search"]
@apidocs( @apidocs(

View File

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

View File

@ -23,6 +23,7 @@ from aiohttp import BodyPartReader
from aiohttp.web import HTTPBadRequest, HTTPCreated from aiohttp.web import HTTPBadRequest, HTTPCreated
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import ClassVar
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.models.repository_paths import RepositoryPaths from ahriman.models.repository_paths import RepositoryPaths
@ -40,7 +41,7 @@ class UploadView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Full POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/service/upload"] ROUTES = ["/api/v1/service/upload"]
@classmethod @classmethod

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import Response, json_response from aiohttp.web import Response, json_response
from typing import ClassVar
from ahriman import __version__ from ahriman import __version__
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -34,7 +35,7 @@ class InfoView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Unauthorized GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/api/v1/info"] ROUTES = ["/api/v1/info"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import Response, json_response from aiohttp.web import Response, json_response
from typing import ClassVar
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs from ahriman.web.apispec.decorators import apidocs
@ -33,7 +34,7 @@ class RepositoriesView(BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Read GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
ROUTES = ["/api/v1/repositories"] ROUTES = ["/api/v1/repositories"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response from aiohttp.web import HTTPBadRequest, HTTPNoContent, Response, json_response
from typing import ClassVar
from ahriman import __version__ from ahriman import __version__
from ahriman.models.build_status import BuildStatusEnum from ahriman.models.build_status import BuildStatusEnum
@ -40,8 +41,8 @@ class StatusView(StatusViewGuard, BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = UserAccess.Read GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Read
POST_PERMISSION = UserAccess.Full POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Full
ROUTES = ["/api/v1/status"] ROUTES = ["/api/v1/status"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPBadRequest, HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized from aiohttp.web import HTTPBadRequest, HTTPFound, HTTPMethodNotAllowed, HTTPUnauthorized
from typing import ClassVar
from ahriman.core.auth.helpers import remember from ahriman.core.auth.helpers import remember
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -35,7 +36,7 @@ class LoginView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized GET_PERMISSION = POST_PERMISSION = UserAccess.Unauthorized # type: ClassVar[UserAccess]
ROUTES = ["/api/v1/login"] ROUTES = ["/api/v1/login"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import HTTPFound, HTTPUnauthorized from aiohttp.web import HTTPFound, HTTPUnauthorized
from typing import ClassVar
from ahriman.core.auth.helpers import check_authorized, forget from ahriman.core.auth.helpers import check_authorized, forget
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
@ -33,7 +34,7 @@ class LogoutView(BaseView):
POST_PERMISSION(UserAccess): (class attribute) post permissions of self POST_PERMISSION(UserAccess): (class attribute) post permissions of self
""" """
POST_PERMISSION = UserAccess.Unauthorized POST_PERMISSION: ClassVar[UserAccess] = UserAccess.Unauthorized
ROUTES = ["/api/v1/logout"] ROUTES = ["/api/v1/logout"]
@apidocs( @apidocs(

View File

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
from aiohttp.web import Response, json_response from aiohttp.web import Response, json_response
from typing import ClassVar
from ahriman.models.user_access import UserAccess from ahriman.models.user_access import UserAccess
from ahriman.web.apispec.decorators import apidocs from ahriman.web.apispec.decorators import apidocs
@ -34,7 +35,7 @@ class LogsView(StatusViewGuard, BaseView):
GET_PERMISSION(UserAccess): (class attribute) get permissions of self GET_PERMISSION(UserAccess): (class attribute) get permissions of self
""" """
GET_PERMISSION = UserAccess.Reporter GET_PERMISSION: ClassVar[UserAccess] = UserAccess.Reporter
ROUTES = ["/api/v2/packages/{package}/logs"] ROUTES = ["/api/v2/packages/{package}/logs"]
@apidocs( @apidocs(

View File

@ -113,6 +113,40 @@ def test_with_dependencies(application: Application, package_ahriman: Package, p
], any_order=True) ], any_order=True)
def test_with_dependencies_exception(application: Application, package_ahriman: Package,
package_python_schedule: Package, mocker: MockerFixture) -> None:
"""
must skip packages if exception occurs
"""
def create_package_mock(package_base) -> MagicMock:
mock = MagicMock()
mock.base = package_base
mock.depends_build = []
mock.packages_full = [package_base]
return mock
package_python_schedule.packages = {
package_python_schedule.base: package_python_schedule.packages[package_python_schedule.base]
}
package_ahriman.packages[package_ahriman.base].depends = ["devtools", "python", package_python_schedule.base]
package_ahriman.packages[package_ahriman.base].make_depends = ["python-build", "python-installer"]
packages = {
package_ahriman.base: package_ahriman,
package_python_schedule.base: package_python_schedule,
"python": create_package_mock("python"),
"python-installer": create_package_mock("python-installer"),
}
mocker.patch("pathlib.Path.is_dir", autospec=True, side_effect=lambda p: p.name == "python")
mocker.patch("ahriman.models.package.Package.from_aur", side_effect=lambda *args: packages[args[0]])
mocker.patch("ahriman.models.package.Package.from_build", side_effect=Exception)
mocker.patch("ahriman.application.application.Application._known_packages",
return_value={"devtools", "python-build", "python-pytest"})
assert not application.with_dependencies([package_ahriman], process_dependencies=True)
def test_with_dependencies_skip(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None: def test_with_dependencies_skip(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must skip processing of dependencies must skip processing of dependencies

View File

@ -19,17 +19,52 @@ def test_migrate(connection: Connection, configuration: Configuration, mocker: M
run_mock.assert_called_once_with() run_mock.assert_called_once_with()
def test_migration(migrations: Migrations) -> None: def test_apply_migrations(migrations: Migrations, mocker: MockerFixture) -> None:
""" """
must perform single migration must apply list of migrations
""" """
migrate_data_mock = MagicMock()
cursor = MagicMock() cursor = MagicMock()
migration = Migration(index=0, name="test", steps=["select 1"], migrate_data=migrate_data_mock) migration = Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock())
migrations.connection.cursor.return_value = cursor
migration_mock = mocker.patch("ahriman.core.database.migrations.Migrations.perform_migration")
migrations.migration(cursor, migration) migrations.apply_migrations([migration])
cursor.execute.assert_called_once_with("select 1") cursor.execute.assert_has_calls([
migrate_data_mock.assert_called_once_with(migrations.connection, migrations.configuration) MockCall("begin exclusive"),
MockCall("commit"),
])
cursor.close.assert_called_once_with()
migration_mock.assert_called_once_with(cursor, migration)
def test_apply_migration_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must roll back and close cursor on exception during migration
"""
cursor = MagicMock()
mocker.patch("logging.Logger.info", side_effect=Exception())
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.apply_migrations([Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock())])
cursor.execute.assert_has_calls([
MockCall("begin exclusive"),
MockCall("rollback"),
])
cursor.close.assert_called_once_with()
def test_apply_migration_sql_exception(migrations: Migrations) -> None:
"""
must close cursor on general migration error
"""
cursor = MagicMock()
cursor.execute.side_effect = Exception()
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.apply_migrations([Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock())])
cursor.close.assert_called_once_with()
def test_migrations(migrations: Migrations) -> None: def test_migrations(migrations: Migrations) -> None:
@ -39,6 +74,19 @@ def test_migrations(migrations: Migrations) -> None:
assert migrations.migrations() assert migrations.migrations()
def test_perform_migration(migrations: Migrations) -> None:
"""
must perform single migration
"""
migrate_data_mock = MagicMock()
cursor = MagicMock()
migration = Migration(index=0, name="test", steps=["select 1"], migrate_data=migrate_data_mock)
migrations.perform_migration(cursor, migration)
cursor.execute.assert_called_once_with("select 1")
migrate_data_mock.assert_called_once_with(migrations.connection, migrations.configuration)
def test_run_skip(migrations: Migrations, mocker: MockerFixture) -> None: def test_run_skip(migrations: Migrations, mocker: MockerFixture) -> None:
""" """
must skip migration if version is the same must skip migration if version is the same
@ -54,60 +102,15 @@ def test_run(migrations: Migrations, mocker: MockerFixture) -> None:
must run migration must run migration
""" """
migration = Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock()) migration = Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock())
cursor = MagicMock()
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0) mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations", return_value=[migration]) mocker.patch("ahriman.core.database.migrations.Migrations.migrations", return_value=[migration])
migrations.connection.cursor.return_value = cursor
migration_mock = mocker.patch("ahriman.core.database.migrations.Migrations.migration")
validate_mock = mocker.patch("ahriman.models.migration_result.MigrationResult.validate") validate_mock = mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
apply_mock = mocker.patch("ahriman.core.database.migrations.Migrations.apply_migrations")
migrations.run() migrations.run()
apply_mock.assert_called_once_with([migration])
validate_mock.assert_called_once_with() validate_mock.assert_called_once_with()
cursor.execute.assert_has_calls([ migrations.connection.execute.assert_called_once_with("pragma user_version = 1")
MockCall("begin exclusive"),
MockCall("pragma user_version = 1"),
MockCall("commit"),
])
cursor.close.assert_called_once_with()
migration_mock.assert_called_once_with(cursor, migration)
def test_run_migration_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must roll back and close cursor on exception during migration
"""
cursor = MagicMock()
mocker.patch("logging.Logger.info", side_effect=Exception())
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock())])
mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.run()
cursor.execute.assert_has_calls([
MockCall("begin exclusive"),
MockCall("rollback"),
])
cursor.close.assert_called_once_with()
def test_run_sql_exception(migrations: Migrations, mocker: MockerFixture) -> None:
"""
must close cursor on general migration error
"""
cursor = MagicMock()
cursor.execute.side_effect = Exception()
mocker.patch("ahriman.core.database.migrations.Migrations.user_version", return_value=0)
mocker.patch("ahriman.core.database.migrations.Migrations.migrations",
return_value=[Migration(index=0, name="test", steps=["select 1"], migrate_data=MagicMock())])
mocker.patch("ahriman.models.migration_result.MigrationResult.validate")
migrations.connection.cursor.return_value = cursor
with pytest.raises(Exception):
migrations.run()
cursor.close.assert_called_once_with()
def test_user_version(migrations: Migrations) -> None: def test_user_version(migrations: Migrations) -> None:

View File

@ -1,19 +1,9 @@
from ahriman.models.log_record_id import LogRecordId from ahriman.models.log_record_id import LogRecordId
def test_eq() -> None: def test_init() -> None:
""" """
must compare two log record identifiers must correctly assign proces identifier if not set
""" """
record1 = LogRecordId("1", "1", "1") assert LogRecordId("1", "2").process_id == LogRecordId.DEFAULT_PROCESS_ID
assert record1 == record1 assert LogRecordId("1", "2", "3").process_id == "3"
record2 = LogRecordId("1", "1")
assert record1 != record2
def test_eq_other() -> None:
"""
must return False in case if object is not an instance of event
"""
assert LogRecordId("1", "1") != 42

View File

@ -3,9 +3,7 @@ import pytest
from io import BytesIO, StringIO from io import BytesIO, StringIO
from pathlib import Path from pathlib import Path
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from unittest.mock import MagicMock
from ahriman.core.exceptions import EncodeError
from ahriman.models.pkgbuild import Pkgbuild from ahriman.models.pkgbuild import Pkgbuild
from ahriman.models.pkgbuild_patch import PkgbuildPatch from ahriman.models.pkgbuild_patch import PkgbuildPatch
@ -46,18 +44,6 @@ def test_from_file_latin(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> N
load_mock.assert_called_once_with(pytest.helpers.anyvar(int)) load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
def test_from_file_unknown_encoding(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
"""
must raise exception when encoding is unknown
"""
open_mock = mocker.patch("pathlib.Path.open")
io_mock = open_mock.return_value.__enter__.return_value = MagicMock()
io_mock.read.return_value.decode.side_effect = EncodeError(pkgbuild_ahriman.DEFAULT_ENCODINGS)
with pytest.raises(EncodeError):
assert Pkgbuild.from_file(Path("local"))
def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None: def test_from_io(pkgbuild_ahriman: Pkgbuild, mocker: MockerFixture) -> None:
""" """
must correctly load from io must correctly load from io

View File

@ -1,8 +1,8 @@
import pytest import pytest
import pytest_asyncio
from aiohttp.test_utils import TestClient from aiohttp.test_utils import TestClient
from aiohttp.web import Application, Resource, UrlMappingMatchInfo from aiohttp.web import Application, Resource, UrlMappingMatchInfo
from asyncio import BaseEventLoop
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from marshmallow import Schema from marshmallow import Schema
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -164,15 +164,13 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
return application return application
@pytest.fixture @pytest_asyncio.fixture
def client(application: Application, event_loop: BaseEventLoop, aiohttp_client: Any, async def client(application: Application, aiohttp_client: Any, mocker: MockerFixture) -> TestClient:
mocker: MockerFixture) -> TestClient:
""" """
web client fixture web client fixture
Args: Args:
application(Application): application fixture application(Application): application fixture
event_loop(BaseEventLoop): context event loop
aiohttp_client(Any): aiohttp client fixture aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object mocker(MockerFixture): mocker object
@ -180,37 +178,35 @@ def client(application: Application, event_loop: BaseEventLoop, aiohttp_client:
TestClient: web client test instance TestClient: web client test instance
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[]) mocker.patch("pathlib.Path.iterdir", return_value=[])
return event_loop.run_until_complete(aiohttp_client(application)) return await aiohttp_client(application)
@pytest.fixture @pytest_asyncio.fixture
def client_with_auth(application_with_auth: Application, event_loop: BaseEventLoop, aiohttp_client: Any, async def client_with_auth(application_with_auth: Application, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture with full authorization functions
Args:
application_with_auth(Application): application fixture
event_loop(BaseEventLoop): context event loop
aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object
Returns:
TestClient: web client test instance
"""
mocker.patch("pathlib.Path.iterdir", return_value=[])
return event_loop.run_until_complete(aiohttp_client(application_with_auth))
@pytest.fixture
def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseEventLoop, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient: mocker: MockerFixture) -> TestClient:
""" """
web client fixture with full authorization functions web client fixture with full authorization functions
Args: Args:
application_with_auth(Application): application fixture application_with_auth(Application): application fixture
event_loop(BaseEventLoop): context event loop aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object
Returns:
TestClient: web client test instance
"""
mocker.patch("pathlib.Path.iterdir", return_value=[])
return await aiohttp_client(application_with_auth)
@pytest_asyncio.fixture
async def client_with_oauth_auth(application_with_auth: Application, aiohttp_client: Any,
mocker: MockerFixture) -> TestClient:
"""
web client fixture with full authorization functions
Args:
application_with_auth(Application): application fixture
aiohttp_client(Any): aiohttp client fixture aiohttp_client(Any): aiohttp client fixture
mocker(MockerFixture): mocker object mocker(MockerFixture): mocker object
@ -219,4 +215,4 @@ def client_with_oauth_auth(application_with_auth: Application, event_loop: BaseE
""" """
mocker.patch("pathlib.Path.iterdir", return_value=[]) mocker.patch("pathlib.Path.iterdir", return_value=[])
application_with_auth[AuthKey] = MagicMock(spec=OAuth) application_with_auth[AuthKey] = MagicMock(spec=OAuth)
return event_loop.run_until_complete(aiohttp_client(application_with_auth)) return await aiohttp_client(application_with_auth)

46
tox.ini
View File

@ -1,9 +1,9 @@
[tox] [tox]
envlist = check, tests envlist = check, tests
isolated_build = True isolated_build = true
labels = labels =
release = version, docs, publish release = version, docs, publish
dependencies = -e .[journald,pacman,s3,shell,stats,validator,web] dependencies = -e .[journald,pacman,reports,s3,shell,stats,unixsocket,validator,web,web_api-docs,web_auth,web_oauth2]
project_name = ahriman project_name = ahriman
[mypy] [mypy]
@ -24,10 +24,13 @@ commands =
[testenv:check] [testenv:check]
description = Run common checks like linter, mypy, etc description = Run common checks like linter, mypy, etc
dependency_groups =
check
deps = deps =
{[tox]dependencies} {[tox]dependencies}
-e .[check] pip_pre = true
setenv = setenv =
CFLAGS="-Wno-unterminated-string-initialization"
MYPYPATH=src MYPYPATH=src
commands = commands =
autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/{[tox]project_name}" "tests/{[tox]project_name}" autopep8 --exit-code --max-line-length 120 -aa -i -j 0 -r "src/{[tox]project_name}" "tests/{[tox]project_name}"
@ -38,16 +41,19 @@ commands =
[testenv:docs] [testenv:docs]
description = Generate source files for documentation description = Generate source files for documentation
depends =
version
deps =
{[tox]dependencies}
-e .[docs]
changedir = src
allowlist_externals = allowlist_externals =
bash bash
find find
mv mv
changedir = src
dependency_groups =
docs
depends =
version
deps =
{[tox]dependencies}
uv
pip_pre = true
setenv = setenv =
SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance
commands = commands =
@ -59,22 +65,26 @@ commands =
# remove autogenerated modules rst files # remove autogenerated modules rst files
find ../docs -type f -name "{[tox]project_name}*.rst" -delete find ../docs -type f -name "{[tox]project_name}*.rst" -delete
sphinx-apidoc -o ../docs . sphinx-apidoc -o ../docs .
# compile list of dependencies for rtd.io
uv pip compile --group ../pyproject.toml:docs --extra s3 --extra validator --extra web --output-file ../docs/requirements.txt --quiet ../pyproject.toml
[testenv:html] [testenv:html]
description = Generate html documentation description = Generate html documentation
dependency_groups =
docs
deps = deps =
{[tox]dependencies} {[tox]dependencies}
-e .[docs] pip_pre = true
recreate = True recreate = true
commands = commands =
sphinx-build -b html -a -j auto -W docs {envtmpdir}{/}html sphinx-build -b html -a -j auto -W docs {envtmpdir}{/}html
[testenv:publish] [testenv:publish]
description = Create and publish release to GitHub description = Create and publish release to GitHub
depends =
docs
allowlist_externals = allowlist_externals =
git git
depends =
docs
passenv = passenv =
SSH_AUTH_SOCK SSH_AUTH_SOCK
commands = commands =
@ -86,18 +96,22 @@ commands =
[testenv:tests] [testenv:tests]
description = Run tests description = Run tests
dependency_groups =
tests
deps = deps =
{[tox]dependencies} {[tox]dependencies}
-e .[tests] pip_pre = true
setenv =
CFLAGS="-Wno-unterminated-string-initialization"
commands = commands =
pytest {posargs} pytest {posargs}
[testenv:version] [testenv:version]
description = Bump package version description = Bump package version
deps =
packaging
allowlist_externals = allowlist_externals =
sed sed
deps =
packaging
commands = commands =
# check if version is set and validate it # check if version is set and validate it
{envpython} -c 'from packaging.version import Version; Version("{posargs}")' {envpython} -c 'from packaging.version import Version; Version("{posargs}")'