Compare commits

..

1 Commits

30 changed files with 664 additions and 880 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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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))

19
tox.ini
View File

@ -3,7 +3,7 @@ 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,10 +24,9 @@ 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 pip_pre = true
setenv = setenv =
CFLAGS="-Wno-unterminated-string-initialization" CFLAGS="-Wno-unterminated-string-initialization"
@ -46,14 +45,11 @@ allowlist_externals =
find find
mv mv
changedir = src changedir = src
dependency_groups =
docs
depends = depends =
version version
deps = deps =
{[tox]dependencies} {[tox]dependencies}
uv -e .[docs]
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 =
@ -65,16 +61,12 @@ 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
@ -96,10 +88,9 @@ 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 pip_pre = true
setenv = setenv =
CFLAGS="-Wno-unterminated-string-initialization" CFLAGS="-Wno-unterminated-string-initialization"