Compare commits

..

2 Commits

Author SHA1 Message Date
7fd8ac8265 website: use date instead of version for listing logs 2025-03-10 17:24:12 +02:00
a03f5c5e6b 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-10 16:24:12 +02:00
43 changed files with 875 additions and 1009 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,9 @@ 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))
@ -28,7 +29,6 @@ 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,13 +91,7 @@ autoclass_content = "both"
autodoc_member_order = "groupwise" autodoc_member_order = "groupwise"
autodoc_mock_imports = [ autodoc_mock_imports = ["cryptography", "pyalpm"]
"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,7 +81,6 @@ Base configuration settings.
* ``apply_migrations`` - perform database migrations on the application start, boolean, optional, default ``yes``. Useful if you are using git version. Note, however, that this option must be changed only if you know what to do and going to handle migrations manually. * ``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
@ -218,7 +217,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,13 +56,6 @@ Though originally I've created ahriman by trying to improve the project, it stil
It is automation tools for ``repoctl`` mentioned above. Except for using shell it looks pretty cool and also offers some additional features like patches, remote synchronization (isn't it?) and reporting. 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
^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^

View File

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

View File

@ -12,22 +12,19 @@ Initial setup
sudo ahriman -a x86_64 -r aur service-setup ... sudo ahriman -a x86_64 -r aur service-setup ...
.. admonition:: Details ``service-setup`` literally does the following steps:
: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``):
.. code-block:: shell echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf
echo 'PACKAGER="ahriman bot <ahriman@example.com>"' | sudo -u ahriman tee -a /var/lib/ahriman/.makepkg.conf #.
Configure build tools (it is required for correct dependency management system):
#. #.
Configure build tools (it is required for correct dependency management system):
#.
Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``): Create build command (you can choose any name for command, basically it should be ``{name}-{arch}-build``):
.. code-block:: shell .. code-block:: shell
@ -70,7 +67,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

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

View File

@ -60,13 +60,10 @@
<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-1 dropend"> <div class="col-2">
<button id="package-info-logs-dropdown" class="btn dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <nav id="package-info-logs-versions" class="nav flex-column"></nav>
<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-11"> <div class="col-10">
<pre class="language-console"><code id="package-info-logs-input" class="pre-scrollable language-console"></code><button id="package-info-logs-copy-button" type="button" class="btn language-console" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre> <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>
@ -312,7 +309,7 @@
) )
.map(version => { .map(version => {
const link = document.createElement("a"); const link = document.createElement("a");
link.classList.add("dropdown-item"); link.classList.add("nav-link");
link.textContent = new Date(1000 * version.created).toISOStringShort(); link.textContent = new Date(1000 * version.created).toISOStringShort();
link.href = "#"; link.href = "#";

View File

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

View File

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

View File

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

View File

@ -25,64 +25,15 @@ 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"
[dependency-groups] [project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.optional-dependencies]
check = [ check = [
"autopep8", "autopep8",
"bandit", "bandit",
@ -96,6 +47,24 @@ 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",
@ -106,6 +75,22 @@ 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

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

View File

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

View File

@ -57,6 +57,10 @@ 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": {
@ -343,6 +347,10 @@ 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",
@ -371,6 +379,11 @@ 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",
@ -379,6 +392,10 @@ 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,7 +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 typing import ClassVar
from ahriman.core.configuration.shell_template import ShellTemplate from ahriman.core.configuration.shell_template import ShellTemplate
@ -85,7 +85,7 @@ class ShellInterpolator(configparser.Interpolation):
"prefix": sys.prefix, "prefix": sys.prefix,
} }
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: Any, option: Any, value: str, def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str,
defaults: Mapping[str, str]) -> str: defaults: Mapping[str, str]) -> str:
""" """
interpolate option value interpolate option value
@ -100,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(Any): section name section(str): section name
option(Any): option name option(str): 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

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

View File

@ -95,6 +95,19 @@ 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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ from pathlib import Path
from typing import Any, ClassVar, 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
@ -33,13 +34,13 @@ class Pkgbuild(Mapping[str, Any]):
model and proxy for PKGBUILD properties model and proxy for PKGBUILD properties
Attributes: Attributes:
DEFAULT_ENCODINGS(str): (class attribute) default encoding to be applied on the file content DEFAULT_ENCODINGS(list[str]): (class attribute) list of encoding to be applied on the file content
fields(dict[str, PkgbuildPatch]): PKGBUILD fields fields(dict[str, PkgbuildPatch]): PKGBUILD fields
""" """
fields: dict[str, PkgbuildPatch] fields: dict[str, PkgbuildPatch]
DEFAULT_ENCODINGS: ClassVar[str] = "utf8" DEFAULT_ENCODINGS: ClassVar[list[str]] = ["utf8", "latin-1"]
@property @property
def variables(self) -> dict[str, str]: def variables(self) -> dict[str, str]:
@ -57,13 +58,13 @@ class Pkgbuild(Mapping[str, Any]):
} }
@classmethod @classmethod
def from_file(cls, path: Path, encoding: str | None = None) -> Self: def from_file(cls, path: Path, encodings: list[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
encoding(str | None, optional): the encoding of the file (Default value = None) encodings(list[str] | None, optional): the encoding of the file (Default value = None)
Returns: Returns:
Self: constructed instance of self Self: constructed instance of self
@ -76,10 +77,15 @@ 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
encoding = encoding or cls.DEFAULT_ENCODINGS encodings = encodings or cls.DEFAULT_ENCODINGS
io = StringIO(content.decode(encoding, errors="backslashreplace")) for encoding in encodings:
try:
io = StringIO(content.decode(encoding))
return cls.from_io(io)
except ValueError:
pass
return cls.from_io(io) raise EncodeError(encodings)
@classmethod @classmethod
def from_io(cls, stream: IO[str]) -> Self: def from_io(cls, stream: IO[str]) -> Self:

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 import aiohttp_cors # type: ignore[import-untyped]
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( # type: ignore[no-untyped-call] "*": aiohttp_cors.ResourceOptions(
expose_headers="*", expose_headers="*",
allow_headers="*", allow_headers="*",
allow_methods="*", allow_methods="*",

View File

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

View File

@ -18,7 +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, Request, StreamResponse, View from aiohttp.web import HTTPBadRequest, HTTPNotFound, Request, StreamResponse, View
from aiohttp_cors import CorsViewMixin from aiohttp_cors import CorsViewMixin # type: ignore[import-untyped]
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from typing import ClassVar, TypeVar from typing import ClassVar, TypeVar

View File

@ -106,7 +106,7 @@ class PackageView(StatusViewGuard, BaseView):
@apidocs( @apidocs(
tags=["Packages"], tags=["Packages"],
summary="Update package", summary="Update package",
description="Update package status and set its descriptor optionally", description="Update package status and set its descriptior optionally",
permission=POST_PERMISSION, permission=POST_PERMISSION,
error_400_enabled=True, error_400_enabled=True,
error_404_description="Repository is unknown", error_404_description="Repository is unknown",

View File

@ -53,7 +53,7 @@ def test_remote_git_url(remote: Remote) -> None:
must raise NotImplemented for missing remote git url must raise NotImplemented for missing remote git url
""" """
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
remote.remote_git_url("package", "repositories") remote.remote_git_url("package", "repositorys")
def test_remote_web_url(remote: Remote) -> None: def test_remote_web_url(remote: Remote) -> None:

View File

@ -10,7 +10,7 @@ from ahriman.core.exceptions import PacmanError
def test_copy(mocker: MockerFixture) -> None: def test_copy(mocker: MockerFixture) -> None:
""" """
must copy local database file must copy loca database file
""" """
copy_mock = mocker.patch("shutil.copy") copy_mock = mocker.patch("shutil.copy")
PacmanDatabase.copy(Path("remote"), Path("local")) PacmanDatabase.copy(Path("remote"), Path("local"))

View File

@ -1,4 +1,3 @@
import pytest
import sqlite3 import sqlite3
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
@ -25,29 +24,15 @@ def test_factory(database: SQLite) -> None:
def test_with_connection(database: SQLite, mocker: MockerFixture) -> None: def test_with_connection(database: SQLite, mocker: MockerFixture) -> None:
""" """
must run query inside connection and close it at the end must run query inside connection
""" """
connection_mock = MagicMock() connection_mock = MagicMock()
connect_mock = mocker.patch("sqlite3.connect", return_value=connection_mock) connect_mock = mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1")) database.with_connection(lambda conn: conn.execute("select 1"))
connect_mock.assert_called_once_with(database.path, detect_types=sqlite3.PARSE_DECLTYPES) connect_mock.assert_called_once_with(database.path, detect_types=sqlite3.PARSE_DECLTYPES)
connection_mock.set_trace_callback.assert_called_once_with(database.logger.debug) connection_mock.__enter__().set_trace_callback.assert_called_once_with(database.logger.debug)
connection_mock.commit.assert_not_called() connection_mock.__enter__().commit.assert_not_called()
connection_mock.close.assert_called_once_with()
def test_with_connection_close(database: SQLite, mocker: MockerFixture) -> None:
"""
must close connection on errors
"""
connection_mock = MagicMock()
connection_mock.commit.side_effect = Exception
mocker.patch("sqlite3.connect", return_value=connection_mock)
with pytest.raises(Exception):
database.with_connection(lambda conn: conn.execute("select 1"), commit=True)
connection_mock.close.assert_called_once_with()
def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) -> None: def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) -> None:
@ -59,4 +44,4 @@ def test_with_connection_with_commit(database: SQLite, mocker: MockerFixture) ->
mocker.patch("sqlite3.connect", return_value=connection_mock) mocker.patch("sqlite3.connect", return_value=connection_mock)
database.with_connection(lambda conn: conn.execute("select 1"), commit=True) database.with_connection(lambda conn: conn.execute("select 1"), commit=True)
connection_mock.commit.assert_called_once_with() connection_mock.__enter__().commit.assert_called_once_with()

View File

@ -285,7 +285,7 @@ def test_set_unknown(client: Client, package_ahriman: Package, mocker: MockerFix
def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None: def test_set_unknown_skip(client: Client, package_ahriman: Package, mocker: MockerFixture) -> None:
""" """
must skip unknown status update in case if package is already known must skip unknown status update in case if pacakge is already known
""" """
mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)]) mocker.patch("ahriman.core.status.Client.package_get", return_value=[(package_ahriman, None)])
update_mock = mocker.patch("ahriman.core.status.Client.package_update") update_mock = mocker.patch("ahriman.core.status.Client.package_update")

View File

@ -73,7 +73,7 @@ def test_configuration_sections(configuration: Configuration) -> None:
def test_on_result(trigger: Trigger) -> None: def test_on_result(trigger: Trigger) -> None:
""" """
must pass execution to run method must pass execution nto run method
""" """
trigger.on_result(Result(), []) trigger.on_result(Result(), [])

View File

@ -3,7 +3,7 @@ from ahriman.models.log_record_id import LogRecordId
def test_init() -> None: def test_init() -> None:
""" """
must correctly assign process identifier if not set must correctly assign proces identifier if not set
""" """
assert LogRecordId("1", "2").process_id == LogRecordId.DEFAULT_PROCESS_ID assert LogRecordId("1", "2").process_id == LogRecordId.DEFAULT_PROCESS_ID
assert LogRecordId("1", "2", "3").process_id == "3" assert LogRecordId("1", "2", "3").process_id == "3"

View File

@ -3,7 +3,9 @@ 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
@ -44,6 +46,18 @@ 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,13 +164,15 @@ def application_with_auth(configuration: Configuration, user: User, spawner: Spa
return application return application
@pytest_asyncio.fixture @pytest.fixture
async def client(application: Application, aiohttp_client: Any, mocker: MockerFixture) -> TestClient: def client(application: Application, event_loop: BaseEventLoop, aiohttp_client: Any,
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
@ -178,35 +180,37 @@ async def client(application: Application, aiohttp_client: Any, mocker: MockerFi
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 await aiohttp_client(application) return event_loop.run_until_complete(aiohttp_client(application))
@pytest_asyncio.fixture @pytest.fixture
async def client_with_auth(application_with_auth: Application, aiohttp_client: Any, def client_with_auth(application_with_auth: Application, event_loop: BaseEventLoop, 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
aiohttp_client(Any): aiohttp client fixture event_loop(BaseEventLoop): context event loop
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
@ -215,4 +219,4 @@ async def client_with_oauth_auth(application_with_auth: Application, aiohttp_cli
""" """
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 await aiohttp_client(application_with_auth) return event_loop.run_until_complete(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,reports,s3,shell,stats,unixsocket,validator,web,web_api-docs,web_auth,web_oauth2] dependencies = -e .[journald,pacman,s3,shell,stats,validator,web]
project_name = ahriman project_name = ahriman
[mypy] [mypy]
@ -24,13 +24,10 @@ 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}
pip_pre = true -e .[check]
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}"
@ -41,19 +38,16 @@ commands =
[testenv:docs] [testenv:docs]
description = Generate source files for documentation description = Generate source files for documentation
allowlist_externals =
bash
find
mv
changedir = src
dependency_groups =
docs
depends = depends =
version version
deps = deps =
{[tox]dependencies} {[tox]dependencies}
uv -e .[docs]
pip_pre = true changedir = src
allowlist_externals =
bash
find
mv
setenv = setenv =
SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance
commands = commands =
@ -65,26 +59,22 @@ 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}
pip_pre = true -e .[docs]
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
allowlist_externals =
git
depends = depends =
docs docs
allowlist_externals =
git
passenv = passenv =
SSH_AUTH_SOCK SSH_AUTH_SOCK
commands = commands =
@ -96,22 +86,18 @@ commands =
[testenv:tests] [testenv:tests]
description = Run tests description = Run tests
dependency_groups =
tests
deps = deps =
{[tox]dependencies} {[tox]dependencies}
pip_pre = true -e .[tests]
setenv =
CFLAGS="-Wno-unterminated-string-initialization"
commands = commands =
pytest {posargs} pytest {posargs}
[testenv:version] [testenv:version]
description = Bump package version description = Bump package version
allowlist_externals =
sed
deps = deps =
packaging packaging
allowlist_externals =
sed
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}")'