Compare commits

..

28 Commits

Author SHA1 Message Date
arcanis e2e013a33b straight forward conftest 2026-07-13 16:33:45 +03:00
arcanis da0853b6f1 generic fixtures 2026-07-13 13:01:54 +03:00
arcanis 7db38ff9e4 reorder tests 2026-07-13 13:00:13 +03:00
arcanis 92ac036f78 migrate to hatch 2026-07-11 12:24:53 +03:00
arcanis aef7fa5f93 build: update frontend dependencies 2026-07-06 16:16:31 +03:00
arcanis c412b08135 fix: remove staleTime defaults for query client 2026-07-06 15:33:20 +03:00
arcanis a3b6372c11 fix: override head method for sse endpoint to make it actually working 2026-07-06 14:44:29 +03:00
arcanis 78288befb8 build: use python 3.13 lowerbound 2026-07-06 13:56:02 +03:00
arcanis 49cd3d43c8 fix: handle errors in sse stream 2026-06-09 01:01:08 +03:00
arcanis d9b52806c0 fix: clear subscriber map on shutdown
Even though it is not a case in the application, the interface could be
used externally
2026-06-07 11:58:04 +03:00
arcanis 774db2d780 type: fix typing ignore with the last typing update 2026-05-31 20:04:25 +03:00
arcanis 68afda4c71 feat: allow readonly events with read permission 2026-05-26 20:51:06 +03:00
arcanis fa9fa73078 docs: bump python version to 2.13 2026-05-08 10:25:24 +03:00
arcanis 3e1e24cb50 feat: SSE support (#162)
* event bus implementation

* update tests

* docs update

* review fixes

* update configs

* fix typo

* frontend changes

* install missing pacakge

* queue processing simplification
2026-05-08 10:20:03 +03:00
arcanis 18fe38c30b build: use build_date argument for docker image instead of guessing from pacman root 2026-05-05 13:45:42 +03:00
arcanis 6ee8b26bd5 feat: show aur url in interface 2026-04-22 06:56:28 +03:00
arcanis fce49f22c9 type: fix mypy warnings in updated version 2026-04-03 13:36:41 +03:00
arcanis af8e2c9e9b build: update rtd.io image 2026-03-30 19:20:03 +03:00
arcanis 1c312bb528 feat: add error boundary 2026-03-24 01:17:42 +02:00
arcanis e39194e9f6 feat: etag support 2026-03-23 23:58:56 +02:00
arcanis 21cc029c18 refactor: use usedforsecurity flag for md5 calculations 2026-03-23 23:07:31 +02:00
arcanis 40671b99d5 feat: allow to configure cors 2026-03-23 16:39:07 +02:00
arcanis 3ad2c494af docs: update docstrings 2026-03-22 17:23:16 +02:00
arcanis 34014d1cdd build: add more rules for typescript 2026-03-22 17:23:16 +02:00
arcanis 93ed2b864b docs: describe rollback procedure in docs 2026-03-22 17:23:16 +02:00
arcanis cca931ccd0 fix: handle errors on pkgbuild reading 2026-03-22 13:10:09 +02:00
arcanis 5e090cebdb refactor: reorder arguments in web ui 2026-03-22 04:03:41 +02:00
arcanis d7984c12f0 feat: package rollback support (#161)
* implement support of rollback handler

* react interface for the rollback
2026-03-22 00:36:48 +02:00
893 changed files with 4359 additions and 2097 deletions
+6
View File
@@ -26,6 +26,10 @@ jobs:
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
- name: Set image date
id: args
run: echo "::set-output name=date::$(date -d yesterday +'%Y-%m-%d')"
- name: Login to docker hub - name: Login to docker hub
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
@@ -53,6 +57,8 @@ jobs:
- name: Build an image and push - name: Build an image and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
build-args: |
BUILD_DATE=${{ steps.args.outputs.date }}
file: docker/Dockerfile file: docker/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
+2 -2
View File
@@ -12,11 +12,11 @@ pacman -Syyu --noconfirm
# main dependencies # main dependencies
pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo pacman -S --noconfirm devtools git npm pyalpm python-bcrypt python-filelock python-inflection python-pyelftools python-requests python-systemd sudo
# make dependencies # make dependencies
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel pacman -S --noconfirm --asdeps base-devel python-build python-hatchling python-installer python-tox python-wheel
# optional dependencies # optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then if [[ -z $MINIMAL_INSTALL ]]; then
# web server # web server
pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-cryptography python-jinja pacman -S --noconfirm python-aioauth-client python-aiohttp python-aiohttp-apispec-git python-aiohttp-cors python-aiohttp-jinja2 python-aiohttp-security python-aiohttp-session python-aiohttp-sse-git python-cryptography python-jinja
# additional features # additional features
pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync
fi fi
+5 -2
View File
@@ -103,5 +103,8 @@ docs/html/
# Frontend # Frontend
node_modules/ node_modules/
package-lock.json package-lock.json
package/share/ahriman/templates/static/index.js ahriman-web/package/share/ahriman/templates/static/index.js
package/share/ahriman/templates/static/index.css ahriman-web/package/share/ahriman/templates/static/index.css
# local configs
/*.ini
+2
View File
@@ -2,4 +2,6 @@
addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec addopts = --cov=ahriman --cov-report=term-missing:skip-covered --no-cov-on-fail --cov-fail-under=100 --spec
asyncio_default_fixture_loop_scope = function asyncio_default_fixture_loop_scope = function
asyncio_mode = auto asyncio_mode = auto
pythonpath = tests
resource-path.directory-name-test-resources = ../../tests/testresources
spec_test_format = {result} {docstring_summary} spec_test_format = {result} {docstring_summary}
+2 -2
View File
@@ -1,9 +1,9 @@
version: 2 version: 2
build: build:
os: ubuntu-20.04 os: ubuntu-lts-latest
tools: tools:
python: "3.12" python: "3.13"
apt_packages: apt_packages:
- graphviz - graphviz
+68
View File
@@ -0,0 +1,68 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "ahriman-core"
description = "ArcH linux ReposItory MANager, core package"
readme = "../README.md"
requires-python = ">=3.13"
license = {file = "../COPYING"}
authors = [
{name = "ahriman team"},
]
dependencies = [
"bcrypt",
"filelock",
"inflection",
"pyelftools",
"requests",
]
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",
]
validator = [
"cerberus",
]
[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"
[tool.hatch.version]
path = "src/ahriman/__init__.py"
[tool.hatch.build.targets.wheel]
packages = ["src/ahriman"]
[tool.hatch.build.targets.wheel.shared-data]
"package/lib" = "lib"
"package/share" = "share"
@@ -156,15 +156,14 @@ class ApplicationRepository(ApplicationProperties):
result = Result() result = Result()
# process already built packages if any # process already built packages if any
built_packages = self.repository.packages_built() if built_packages := self.repository.packages_built(): # speedup a bit
if built_packages: # speedup a bit
build_result = self.repository.process_update(built_packages, packagers) build_result = self.repository.process_update(built_packages, packagers)
self.on_result(build_result) self.on_result(build_result)
result.merge(build_result) result.merge(build_result)
# filter packages which were prebuilt # filter packages which were prebuilt
succeeded = {package.base for package in build_result.success} succeeded = {package.base for package in build_result.success}
updates = filter(lambda package: package.base not in succeeded, updates) updates = [package for package in updates if package.base not in succeeded]
builder = Updater.load(self.repository_id, self.configuration, self.repository) builder = Updater.load(self.repository_id, self.configuration, self.repository)
@@ -19,6 +19,7 @@
# #
import argparse import argparse
from dataclasses import replace
from pathlib import Path from pathlib import Path
from ahriman.application.application import Application from ahriman.application.application import Application
@@ -62,9 +63,9 @@ class Rollback(Handler):
application.reporter.package_hold_update(package.base, enabled=True) application.reporter.package_hold_update(package.base, enabled=True)
@staticmethod @staticmethod
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser: def _set_package_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
""" """
add parser for package archives subcommand add parser for package rollback subcommand
Args: Args:
root(SubParserAction): subparsers for the commands root(SubParserAction): subparsers for the commands
@@ -80,14 +81,14 @@ class Rollback(Handler):
action=argparse.BooleanOptionalAction, default=True) action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-u", "--username", help="build as user", default=extract_user()) parser.add_argument("-u", "--username", help="build as user", default=extract_user())
parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False, parser.set_defaults(aur=False, changes=False, check_files=False, dependencies=False, dry_run=False,
exit_code=False, increment=False, now=True, local=False, manual=False, refresh=False, exit_code=True, increment=False, now=True, local=False, manual=False, refresh=False,
source=PackageSource.Archive, variable=None, vcs=False) source=PackageSource.Archive, variable=None, vcs=False)
return parser return parser
@staticmethod @staticmethod
def package_artifacts(application: Application, package: Package) -> list[Path]: def package_artifacts(application: Application, package: Package) -> list[Path]:
""" """
look for package artifacts and returns paths to them if any look for requested package artifacts and return paths to them
Args: Args:
application(Application): application instance application(Application): application instance
@@ -102,13 +103,13 @@ class Rollback(Handler):
# lookup for built artifacts # lookup for built artifacts
artifacts = application.repository.package_archives_lookup(package) artifacts = application.repository.package_archives_lookup(package)
if not artifacts: if not artifacts:
raise UnknownPackageError(package.base) from None raise UnknownPackageError(package.base)
return artifacts return artifacts
@staticmethod @staticmethod
def package_load(application: Application, package_base: str, version: str) -> Package: def package_load(application: Application, package_base: str, version: str) -> Package:
""" """
load package from given arguments load package from repository, while setting requested version
Args: Args:
application(Application): application instance application(Application): application instance
@@ -123,10 +124,8 @@ class Rollback(Handler):
""" """
try: try:
package, _ = next(iter(application.reporter.package_get(package_base))) package, _ = next(iter(application.reporter.package_get(package_base)))
package.version = version return replace(package, version=version)
return package
except StopIteration: except StopIteration:
raise UnknownPackageError(package_base) from None raise UnknownPackageError(package_base) from None
arguments = [_set_package_archives_parser] arguments = [_set_package_rollback_parser]
@@ -104,7 +104,7 @@ class PkgbuildParser(shlex.shlex):
# ignore substitution and extend bash symbols # ignore substitution and extend bash symbols
self.wordchars += "${}#:+-@!" self.wordchars += "${}#:+-@!"
# in case of default behaviour, it will ignore, for example, segment part of url outside of quotes # in case of default behavior, it will ignore, for example, segment part of url outside of quotes
self.commenters = "" self.commenters = ""
@staticmethod @staticmethod
@@ -72,7 +72,7 @@ class AUR(Remote):
parse RPC response to package list parse RPC response to package list
Args: Args:
response(dict[str, Any]): RPC response json response(dict[str, Any]): RPC response JSON
Returns: Returns:
list[AURPackage]: list of parsed packages list[AURPackage]: list of parsed packages
@@ -74,7 +74,7 @@ class Official(Remote):
parse RPC response to package list parse RPC response to package list
Args: Args:
response(dict[str, Any]): RPC response json response(dict[str, Any]): RPC response JSON
Returns: Returns:
list[AURPackage]: list of parsed packages list[AURPackage]: list of parsed packages
@@ -32,7 +32,7 @@ class OfficialSyncdb(Official):
updates. updates.
This approach also has limitations, because we don't require superuser rights (neither going to download database This approach also has limitations, because we don't require superuser rights (neither going to download database
separately), the database file might be outdated and must be handled manually (or kind of). This behaviour might be separately), the database file might be outdated and must be handled manually (or kind of). This behavior might be
changed in the future. changed in the future.
Still we leave search function based on the official repositories RPC. Still we leave search function based on the official repositories RPC.
@@ -72,17 +72,17 @@ class PackageArchive:
if not PackageArchive.is_elf(binary_file): if not PackageArchive.is_elf(binary_file):
return [] return []
elf_file = ELFFile(binary_file) # type: ignore[no-untyped-call] elf_file = ELFFile(binary_file)
dynamic_section = next( dynamic_section = next(
(section for section in elf_file.iter_sections() # type: ignore[no-untyped-call] (section for section in elf_file.iter_sections()
if isinstance(section, DynamicSection)), if isinstance(section, DynamicSection)),
None) None)
if dynamic_section is None: if dynamic_section is None:
return [] return []
return [ return [
tag.needed tag.needed # type: ignore[attr-defined]
for tag in dynamic_section.iter_tags() # type: ignore[no-untyped-call] for tag in dynamic_section.iter_tags()
if tag.entry.d_tag == "DT_NEEDED" if tag.entry.d_tag == "DT_NEEDED"
] ]
@@ -416,7 +416,7 @@ class Sources(LazyLogging):
else: else:
patch.write(sources_dir / "PKGBUILD") patch.write(sources_dir / "PKGBUILD")
def read(self, sources_dir: Path, commit_sha: str, path: Path) -> str: def read(self, sources_dir: Path, commit_sha: str, path: Path) -> str | None:
""" """
read file content from the specified commit read file content from the specified commit
@@ -426,6 +426,10 @@ class Sources(LazyLogging):
path(Path): path to file inside the repository path(Path): path to file inside the repository
Returns: Returns:
str: file content at specified commit str | None: file content at specified commit if available
""" """
return check_output(*self.git(), "show", f"{commit_sha}:{path}", cwd=sources_dir, logger=self.logger) try:
return check_output(*self.git(), "show", f"{commit_sha}:{path}", cwd=sources_dir, logger=self.logger)
except CalledProcessError:
self.logger.exception("failed to read file %s at %s", path, commit_sha)
return None
@@ -72,7 +72,7 @@ class Configuration(configparser.RawConfigParser):
def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None: def __init__(self, allow_no_value: bool = False, allow_multi_key: bool = True) -> None:
""" """
Args: Args:
allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behaviour. In case if it is set allow_no_value(bool, optional): copies :class:`configparser.RawConfigParser` behavior. In case if it is set
to ``True``, the keys without values will be allowed (Default value = False) to ``True``, the keys without values will be allowed (Default value = False)
allow_multi_key(bool, optional): if set to ``False``, then the default dictionary class will be used to allow_multi_key(bool, optional): if set to ``False``, then the default dictionary class will be used to
store keys internally. Otherwise, the special implementation will be used, which supports arrays store keys internally. Otherwise, the special implementation will be used, which supports arrays
@@ -80,7 +80,7 @@ class Configuration(configparser.RawConfigParser):
""" """
configparser.RawConfigParser.__init__( configparser.RawConfigParser.__init__(
self, self,
dict_type=ConfigurationMultiDict if allow_multi_key else dict, # type: ignore[arg-type] dict_type=ConfigurationMultiDict if allow_multi_key else dict,
allow_no_value=allow_no_value, allow_no_value=allow_no_value,
strict=False, strict=False,
empty_lines_in_values=not allow_multi_key, empty_lines_in_values=not allow_multi_key,
@@ -358,6 +358,38 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"min": 0, "min": 0,
}, },
}, },
"cors_allow_headers": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_allow_methods": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_allow_origins": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"cors_expose_headers": {
"type": "list",
"coerce": "list",
"schema": {
"type": "string",
"empty": False,
},
},
"enable_archive_upload": { "enable_archive_upload": {
"type": "boolean", "type": "boolean",
"coerce": "boolean", "coerce": "boolean",
@@ -150,6 +150,6 @@ class ShellTemplate(Template):
break break
kwargs.update(mapping) kwargs.update(mapping)
substituted = dict(generator(kwargs)) kwargs.update(dict(generator(kwargs)))
return self.safe_substitute(kwargs | substituted) return self.safe_substitute(kwargs)

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