mirror of
https://github.com/arcan1s/ahriman.git
synced 2025-06-28 14:51:43 +00:00
Compare commits
2 Commits
c8f25f8769
...
aad94d7b8a
Author | SHA1 | Date | |
---|---|---|---|
aad94d7b8a | |||
6ea56faede |
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@ -8,6 +8,10 @@ on:
|
||||
- '*'
|
||||
- '!*rc*'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker-image:
|
||||
|
||||
|
3
.github/workflows/regress.yml
vendored
3
.github/workflows/regress.yml
vendored
@ -2,6 +2,9 @@ name: Regress
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-regress-tests:
|
||||
|
||||
|
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@ -5,6 +5,9 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
make-release:
|
||||
|
||||
|
3
.github/workflows/setup.yml
vendored
3
.github/workflows/setup.yml
vendored
@ -8,6 +8,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-setup-minimal:
|
||||
|
||||
|
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@ -10,6 +10,9 @@ on:
|
||||
schedule:
|
||||
- cron: 1 0 * * *
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
|
||||
|
@ -9,13 +9,7 @@ build:
|
||||
|
||||
python:
|
||||
install:
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- s3
|
||||
- validator
|
||||
- web
|
||||
- requirements: docs/requirements.txt
|
||||
|
||||
formats:
|
||||
- pdf
|
||||
|
12
docs/conf.py
12
docs/conf.py
@ -15,9 +15,8 @@ import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman import __version__
|
||||
|
||||
|
||||
# support package imports
|
||||
basedir = Path(__file__).resolve().parent.parent / "src"
|
||||
sys.path.insert(0, str(basedir))
|
||||
|
||||
@ -29,6 +28,7 @@ copyright = f"2021-{datetime.date.today().year}, ahriman team"
|
||||
author = "ahriman team"
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
from ahriman import __version__
|
||||
release = __version__
|
||||
|
||||
|
||||
@ -91,7 +91,13 @@ autoclass_content = "both"
|
||||
|
||||
autodoc_member_order = "groupwise"
|
||||
|
||||
autodoc_mock_imports = ["cryptography", "pyalpm"]
|
||||
autodoc_mock_imports = [
|
||||
"aioauth_client",
|
||||
"aiohttp_security",
|
||||
"aiohttp_session",
|
||||
"cryptography",
|
||||
"pyalpm",
|
||||
]
|
||||
|
||||
autodoc_default_options = {
|
||||
"no-undoc-members": True,
|
||||
|
128
docs/requirements.txt
Normal file
128
docs/requirements.txt
Normal 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
|
@ -25,15 +25,64 @@ dependencies = [
|
||||
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
journald = [
|
||||
"systemd-python",
|
||||
]
|
||||
# FIXME technically this dependency is required, but in some cases we do not have access to
|
||||
# the libalpm which is required in order to install the package. Thus in case if we do not
|
||||
# really need to run the application we can move it to "optional" dependencies
|
||||
pacman = [
|
||||
"pyalpm",
|
||||
]
|
||||
reports = [
|
||||
"Jinja2",
|
||||
]
|
||||
s3 = [
|
||||
"boto3",
|
||||
]
|
||||
shell = [
|
||||
"IPython"
|
||||
]
|
||||
stats = [
|
||||
"matplotlib",
|
||||
]
|
||||
unixsocket = [
|
||||
"requests-unixsocket2", # required by unix socket support
|
||||
]
|
||||
validator = [
|
||||
"cerberus",
|
||||
]
|
||||
web = [
|
||||
"aiohttp",
|
||||
"aiohttp_cors",
|
||||
"aiohttp_jinja2",
|
||||
]
|
||||
web_api-docs = [
|
||||
"ahriman[web]",
|
||||
"aiohttp-apispec",
|
||||
"setuptools", # required by aiohttp-apispec
|
||||
]
|
||||
web_auth = [
|
||||
"ahriman[web]",
|
||||
"aiohttp_session",
|
||||
"aiohttp_security",
|
||||
"cryptography",
|
||||
]
|
||||
web_oauth2 = [
|
||||
"ahriman[web_auth]",
|
||||
"aioauth-client",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
ahriman = "ahriman.application.ahriman:run"
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://ahriman.readthedocs.io/"
|
||||
Repository = "https://github.com/arcan1s/ahriman"
|
||||
Changelog = "https://github.com/arcan1s/ahriman/releases"
|
||||
|
||||
[project.scripts]
|
||||
ahriman = "ahriman.application.ahriman:run"
|
||||
|
||||
[project.optional-dependencies]
|
||||
[dependency-groups]
|
||||
check = [
|
||||
"autopep8",
|
||||
"bandit",
|
||||
@ -47,24 +96,6 @@ docs = [
|
||||
"shtab",
|
||||
"sphinx-argparse",
|
||||
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734
|
||||
]
|
||||
journald = [
|
||||
"systemd-python",
|
||||
]
|
||||
# FIXME technically this dependency is required, but in some cases we do not have access to
|
||||
# the libalpm which is required in order to install the package. Thus in case if we do not
|
||||
# really need to run the application we can move it to "optional" dependencies
|
||||
pacman = [
|
||||
"pyalpm",
|
||||
]
|
||||
s3 = [
|
||||
"boto3",
|
||||
]
|
||||
shell = [
|
||||
"IPython"
|
||||
]
|
||||
stats = [
|
||||
"matplotlib",
|
||||
]
|
||||
tests = [
|
||||
"pytest",
|
||||
@ -75,22 +106,6 @@ tests = [
|
||||
"pytest-resource-path",
|
||||
"pytest-spec",
|
||||
]
|
||||
validator = [
|
||||
"cerberus",
|
||||
]
|
||||
web = [
|
||||
"Jinja2",
|
||||
"aioauth-client",
|
||||
"aiohttp",
|
||||
"aiohttp-apispec",
|
||||
"aiohttp_cors",
|
||||
"aiohttp_jinja2",
|
||||
"aiohttp_session",
|
||||
"aiohttp_security",
|
||||
"cryptography",
|
||||
"requests-unixsocket2", # required by unix socket support
|
||||
"setuptools", # required by aiohttp-apispec
|
||||
]
|
||||
|
||||
[tool.flit.sdist]
|
||||
include = [
|
||||
|
183
src/ahriman/core/alpm/bytes_pkgbuild_parser.py
Normal file
183
src/ahriman/core/alpm/bytes_pkgbuild_parser.py
Normal 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"))
|
46
tox.ini
46
tox.ini
@ -1,9 +1,9 @@
|
||||
[tox]
|
||||
envlist = check, tests
|
||||
isolated_build = True
|
||||
isolated_build = true
|
||||
labels =
|
||||
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
|
||||
|
||||
[mypy]
|
||||
@ -24,10 +24,13 @@ commands =
|
||||
|
||||
[testenv:check]
|
||||
description = Run common checks like linter, mypy, etc
|
||||
dependency_groups =
|
||||
check
|
||||
deps =
|
||||
{[tox]dependencies}
|
||||
-e .[check]
|
||||
pip_pre = true
|
||||
setenv =
|
||||
CFLAGS="-Wno-unterminated-string-initialization"
|
||||
MYPYPATH=src
|
||||
commands =
|
||||
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]
|
||||
description = Generate source files for documentation
|
||||
depends =
|
||||
version
|
||||
deps =
|
||||
{[tox]dependencies}
|
||||
-e .[docs]
|
||||
changedir = src
|
||||
allowlist_externals =
|
||||
bash
|
||||
find
|
||||
mv
|
||||
changedir = src
|
||||
dependency_groups =
|
||||
docs
|
||||
depends =
|
||||
version
|
||||
deps =
|
||||
{[tox]dependencies}
|
||||
uv
|
||||
pip_pre = true
|
||||
setenv =
|
||||
SPHINX_APIDOC_OPTIONS=members,no-undoc-members,show-inheritance
|
||||
commands =
|
||||
@ -59,22 +65,26 @@ commands =
|
||||
# remove autogenerated modules rst files
|
||||
find ../docs -type f -name "{[tox]project_name}*.rst" -delete
|
||||
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]
|
||||
description = Generate html documentation
|
||||
dependency_groups =
|
||||
docs
|
||||
deps =
|
||||
{[tox]dependencies}
|
||||
-e .[docs]
|
||||
recreate = True
|
||||
pip_pre = true
|
||||
recreate = true
|
||||
commands =
|
||||
sphinx-build -b html -a -j auto -W docs {envtmpdir}{/}html
|
||||
|
||||
[testenv:publish]
|
||||
description = Create and publish release to GitHub
|
||||
depends =
|
||||
docs
|
||||
allowlist_externals =
|
||||
git
|
||||
depends =
|
||||
docs
|
||||
passenv =
|
||||
SSH_AUTH_SOCK
|
||||
commands =
|
||||
@ -86,18 +96,22 @@ commands =
|
||||
|
||||
[testenv:tests]
|
||||
description = Run tests
|
||||
dependency_groups =
|
||||
tests
|
||||
deps =
|
||||
{[tox]dependencies}
|
||||
-e .[tests]
|
||||
pip_pre = true
|
||||
setenv =
|
||||
CFLAGS="-Wno-unterminated-string-initialization"
|
||||
commands =
|
||||
pytest {posargs}
|
||||
|
||||
[testenv:version]
|
||||
description = Bump package version
|
||||
deps =
|
||||
packaging
|
||||
allowlist_externals =
|
||||
sed
|
||||
deps =
|
||||
packaging
|
||||
commands =
|
||||
# check if version is set and validate it
|
||||
{envpython} -c 'from packaging.version import Version; Version("{posargs}")'
|
||||
|
Reference in New Issue
Block a user