mirror of
https://github.com/arcan1s/ahriman.git
synced 2026-07-13 22:31:07 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e0f1563f62 |
@@ -0,0 +1,6 @@
|
||||
skips:
|
||||
- B101
|
||||
- B104
|
||||
- B105
|
||||
- B106
|
||||
- B404
|
||||
@@ -12,6 +12,3 @@ __pycache__/
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.pyo
|
||||
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
@@ -7,11 +7,6 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
- '!*rc*'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
docker-image:
|
||||
@@ -22,22 +17,18 @@ jobs:
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set image date
|
||||
id: args
|
||||
run: echo "::set-output name=date::$(date -d yesterday +'%Y-%m-%d')"
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to docker hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to github container registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -45,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Extract docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: |
|
||||
arcan1s/ahriman
|
||||
@@ -55,10 +46,8 @@ jobs:
|
||||
type=edge
|
||||
|
||||
- name: Build an image and push
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
build-args: |
|
||||
BUILD_DATE=${{ steps.args.outputs.date }}
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
name: Regress
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: 1 0 * * 0
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
run-regress-tests:
|
||||
@@ -37,6 +31,8 @@ jobs:
|
||||
- repo:/var/lib/ahriman
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- run: pacman -Sy
|
||||
|
||||
- name: Init repository
|
||||
|
||||
@@ -5,24 +5,13 @@ on:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
make-release:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: archlinux:base
|
||||
options: -w /build
|
||||
volumes:
|
||||
- ${{ github.workspace }}:/build
|
||||
|
||||
steps:
|
||||
- run: pacman --noconfirm -Syu base-devel git python-tox
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
@@ -35,13 +24,18 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
filter: 'Release \d+\.\d+\.\d+'
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ConorMacBride/install-package@v1.1.0
|
||||
with:
|
||||
apt: tox
|
||||
|
||||
- name: Create archive
|
||||
run: tox -e archive
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.VERSION }}
|
||||
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body: |
|
||||
${{ steps.changelog.outputs.compareurl }}
|
||||
|
||||
@@ -10,13 +10,13 @@ echo -e '[arcanisrepo]\nServer = https://repo.arcanis.me/$arch\nSigLevel = Never
|
||||
# refresh the image
|
||||
pacman -Syyu --noconfirm
|
||||
# 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 pyalpm python-bcrypt python-inflection python-pyelftools python-requests python-systemd sudo
|
||||
# make dependencies
|
||||
pacman -S --noconfirm --asdeps base-devel python-build python-hatchling python-installer python-tox python-wheel
|
||||
pacman -S --noconfirm --asdeps base-devel python-build python-flit python-installer python-tox python-wheel
|
||||
# optional dependencies
|
||||
if [[ -z $MINIMAL_INSTALL ]]; then
|
||||
# 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-aiohttp-sse-git 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-cryptography python-jinja
|
||||
# additional features
|
||||
pacman -S --noconfirm gnupg ipython python-boto3 python-cerberus python-matplotlib rsync
|
||||
fi
|
||||
|
||||
@@ -7,10 +7,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-setup-minimal:
|
||||
@@ -24,7 +20,7 @@ jobs:
|
||||
- ${{ github.workspace }}:/build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup the minimal service in arch linux container
|
||||
run: .github/workflows/setup.sh minimal
|
||||
@@ -40,7 +36,7 @@ jobs:
|
||||
options: --privileged -w /build
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup the service in arch linux container
|
||||
run: .github/workflows/setup.sh
|
||||
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
# Install dependencies and run test in container
|
||||
|
||||
set -ex
|
||||
|
||||
# install dependencies
|
||||
pacman --noconfirm -Syyu base-devel python-tox
|
||||
|
||||
# run test and check targets
|
||||
tox
|
||||
@@ -9,10 +9,6 @@ on:
|
||||
- master
|
||||
schedule:
|
||||
- cron: 1 0 * * *
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
@@ -26,16 +22,7 @@ jobs:
|
||||
- ${{ github.workspace }}:/build
|
||||
|
||||
steps:
|
||||
- run: pacman --noconfirm -Syu base-devel git npm python-tox
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- run: git config --global --add safe.directory *
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run check and tests
|
||||
run: tox
|
||||
|
||||
- name: Generate documentation and check if there are untracked changes
|
||||
run: |
|
||||
tox -e docs
|
||||
[ -z "$(git status --porcelain docs/*.rst)" ]
|
||||
- name: Run check and tests in arch linux container
|
||||
run: .github/workflows/tests.sh
|
||||
|
||||
@@ -99,12 +99,3 @@ status_cache.json
|
||||
*.db
|
||||
|
||||
docs/html/
|
||||
|
||||
# Frontend
|
||||
node_modules/
|
||||
package-lock.json
|
||||
ahriman-web/package/share/ahriman/templates/static/index.js
|
||||
ahriman-web/package/share/ahriman/templates/static/index.css
|
||||
|
||||
# local configs
|
||||
/*.ini
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
[tool.pylint.main]
|
||||
init-hook = "sys.path.append('tools')"
|
||||
load-plugins = [
|
||||
"pylint.extensions.docparams",
|
||||
"pylint.extensions.bad_builtin",
|
||||
"pylint_plugins.definition_order",
|
||||
"pylint_plugins.import_order",
|
||||
]
|
||||
|
||||
[tool.pylint.classes]
|
||||
bad-functions = [
|
||||
"print",
|
||||
]
|
||||
|
||||
[tool.pylint.design]
|
||||
max-parents = 15
|
||||
|
||||
[tool.pylint."messages control"]
|
||||
disable = [
|
||||
"raw-checker-failed",
|
||||
"bad-inline-option",
|
||||
"locally-disabled",
|
||||
"file-ignored",
|
||||
"suppressed-message",
|
||||
"useless-suppression",
|
||||
"deprecated-pragma",
|
||||
"use-symbolic-message-instead",
|
||||
"use-implicit-booleaness-not-comparison-to-string",
|
||||
"use-implicit-booleaness-not-comparison-to-zero",
|
||||
"missing-module-docstring",
|
||||
"line-too-long",
|
||||
"no-name-in-module",
|
||||
"import-outside-toplevel",
|
||||
"invalid-name",
|
||||
"raise-missing-from",
|
||||
"wrong-import-order",
|
||||
"too-few-public-methods",
|
||||
"too-many-instance-attributes",
|
||||
"broad-exception-caught",
|
||||
"fixme",
|
||||
"too-many-arguments",
|
||||
"duplicate-code",
|
||||
"cyclic-import",
|
||||
"too-many-positional-arguments",
|
||||
]
|
||||
@@ -0,0 +1,651 @@
|
||||
[MAIN]
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
|
||||
# in a server-like mode.
|
||||
clear-cache-post-run=no
|
||||
|
||||
# Load and enable all available extensions. Use --list-extensions to see a list
|
||||
# all available extensions.
|
||||
#enable-all-extensions=
|
||||
|
||||
# In error mode, messages with a category besides ERROR or FATAL are
|
||||
# suppressed, and no reports are done by default. Error mode is compatible with
|
||||
# disabling specific errors.
|
||||
#errors-only=
|
||||
|
||||
# Always return a 0 (non-error) status code, even if lint errors are found.
|
||||
# This is primarily useful in continuous integration scripts.
|
||||
#exit-zero=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold under which the program will exit with error.
|
||||
fail-under=10
|
||||
|
||||
# Interpret the stdin as a python script, whose filename needs to be passed as
|
||||
# the module_or_package argument.
|
||||
#from-stdin=
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regular expressions patterns to the
|
||||
# ignore-list. The regex matches against paths and can be in Posix or Windows
|
||||
# format. Because '\\' represents the directory delimiter on Windows systems,
|
||||
# it can't be used as an escape character.
|
||||
ignore-paths=
|
||||
|
||||
# Files or directories matching the regular expression patterns are skipped.
|
||||
# The regex matches against base names, not paths. The default value ignores
|
||||
# Emacs file locks
|
||||
ignore-patterns=^\.#
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
init-hook='import sys; sys.path.append("pylint_plugins")'
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
# avoid hangs.
|
||||
jobs=1
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint.extensions.docparams,
|
||||
pylint.extensions.bad_builtin,
|
||||
definition_order,
|
||||
import_order,
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.11
|
||||
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=no
|
||||
|
||||
# Add paths to the list of the source roots. Supports globbing patterns. The
|
||||
# source root is an absolute path or a path relative to the current working
|
||||
# directory used to determine a package namespace for modules located under the
|
||||
# source root.
|
||||
source-roots=
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# In verbose mode, extra non-checker-related info will be displayed.
|
||||
#verbose=
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style. If left empty, argument names will be checked with the set
|
||||
# naming style.
|
||||
#argument-rgx=
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style. If left empty, attribute names will be checked with the set naming
|
||||
# style.
|
||||
#attr-rgx=
|
||||
|
||||
bad-functions=print,
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style. If left empty, class attribute names will be checked
|
||||
# with the set naming style.
|
||||
#class-attribute-rgx=
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style. If left empty, class constant names will be checked with
|
||||
# the set naming style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style. If left empty, class names will be checked with the set naming style.
|
||||
#class-rgx=
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style. If left empty, constant names will be checked with the set naming
|
||||
# style.
|
||||
#const-rgx=
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style. If left empty, function names will be checked with the set
|
||||
# naming style.
|
||||
#function-rgx=
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style. If left empty, inline iteration names will be checked
|
||||
# with the set naming style.
|
||||
#inlinevar-rgx=
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style. If left empty, method names will be checked with the set naming style.
|
||||
#method-rgx=
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style. If left empty, module names will be checked with the set naming style.
|
||||
#module-rgx=
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct type alias names. If left empty, type
|
||||
# alias names will be checked with the set naming style.
|
||||
#typealias-rgx=
|
||||
|
||||
# Regular expression matching correct type variable names. If left empty, type
|
||||
# variable names will be checked with the set naming style.
|
||||
#typevar-rgx=
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style. If left empty, variable names will be checked with the set
|
||||
# naming style.
|
||||
#variable-rgx=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp,
|
||||
asyncSetUp,
|
||||
__post_init__
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=15
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when caught.
|
||||
overgeneral-exceptions=builtins.BaseException,builtins.Exception
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow explicit reexports by alias from a package __init__.
|
||||
allow-reexport-from-package=no
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
|
||||
# UNDEFINED.
|
||||
confidence=HIGH,
|
||||
CONTROL_FLOW,
|
||||
INFERENCE,
|
||||
INFERENCE_FAILURE,
|
||||
UNDEFINED
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then re-enable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=raw-checker-failed,
|
||||
bad-inline-option,
|
||||
locally-disabled,
|
||||
file-ignored,
|
||||
suppressed-message,
|
||||
useless-suppression,
|
||||
deprecated-pragma,
|
||||
use-symbolic-message-instead,
|
||||
missing-module-docstring,
|
||||
line-too-long,
|
||||
no-name-in-module,
|
||||
import-outside-toplevel,
|
||||
invalid-name,
|
||||
raise-missing-from,
|
||||
wrong-import-order,
|
||||
too-few-public-methods,
|
||||
too-many-instance-attributes,
|
||||
broad-except,
|
||||
fixme,
|
||||
too-many-arguments,
|
||||
duplicate-code,
|
||||
cyclic-import,
|
||||
too-many-positional-arguments,
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=c-extension-no-member
|
||||
|
||||
|
||||
[METHOD_ARGS]
|
||||
|
||||
# List of qualified names (i.e., library.method) which require a timeout
|
||||
# parameter e.g. 'requests.api.get,requests.api.post'
|
||||
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
notes-rgx=
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
|
||||
# 'convention', and 'info' which contain the number of messages in each
|
||||
# category, as well as 'statement' which is the total number of statements
|
||||
# analyzed. This score is used by the global evaluation report (RP0004).
|
||||
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
#output-format=
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=yes
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. No available dictionaries : You need to install
|
||||
# both the python package and the system dependency for enchant to work..
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear at the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of symbolic message names to ignore for Mixin members.
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins.
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||
|
||||
# Argument names that match this expression will be ignored.
|
||||
ignored-argument-names=_.*|^ignored_|^unused_
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||
@@ -1,7 +0,0 @@
|
||||
[pytest]
|
||||
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_mode = auto
|
||||
pythonpath = tests
|
||||
resource-path.directory-name-test-resources = ../../tests/testresources
|
||||
spec_test_format = {result} {docstring_summary}
|
||||
+9
-3
@@ -1,15 +1,21 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-lts-latest
|
||||
os: ubuntu-20.04
|
||||
tools:
|
||||
python: "3.13"
|
||||
python: "3.12"
|
||||
apt_packages:
|
||||
- graphviz
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
||||
extra_requirements:
|
||||
- docs
|
||||
- s3
|
||||
- validator
|
||||
- web
|
||||
|
||||
formats:
|
||||
- pdf
|
||||
|
||||
+4
-14
@@ -80,7 +80,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
>>> clazz = Clazz()
|
||||
"""
|
||||
|
||||
CLAZZ_ATTRIBUTE: ClassVar[int] = 42
|
||||
CLAZZ_ATTRIBUTE = 42
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
@@ -96,7 +96,6 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
* Type annotations are the must, even for local functions. For the function argument `self` (for instance methods) and `cls` (for class methods) should not be annotated.
|
||||
* For collection types built-in classes must be used if possible (e.g. `dict` instead of `typing.Dict`, `tuple` instead of `typing.Tuple`). In case if built-in type is not available, but `collections.abc` provides interface, it must be used (e.g. `collections.abc.Awaitable` instead of `typing.Awaitable`, `collections.abc.Iterable` instead of `typing.Iterable`). For union classes, the bar operator (`|`) must be used (e.g. `float | int` instead of `typing.Union[float, int]`), which also includes `typing.Optional` (e.g. `str | None` instead of `Optional[str]`).
|
||||
* `classmethod` should (almost) always return `Self`. In case of mypy warning (e.g. if there is a branch in which function doesn't return the instance of `cls`) consider using `staticmethod` instead.
|
||||
* Class attributes must be decorated as `ClassVar[...]`.
|
||||
* Recommended order of function definitions in class:
|
||||
|
||||
```python
|
||||
@@ -125,7 +124,7 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
def __hash__(self) -> int: ... # basically any magic (or look-alike) method
|
||||
```
|
||||
|
||||
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined. Same idea applies to frontend classes.
|
||||
Methods inside one group should be ordered alphabetically, the only exceptions are `__init__` (`__post_init__` for dataclasses), `__new__` and `__del__` methods which should be defined first. For test methods it is recommended to follow the order in which functions are defined.
|
||||
|
||||
Though, we would like to highlight abstract methods (i.e. ones which raise `NotImplementedError`), we still keep in global order at the moment.
|
||||
|
||||
@@ -165,16 +164,10 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
|
||||
# Blank line again and package imports
|
||||
from ahriman.core.configuration import Configuration
|
||||
# Multiline import example
|
||||
from ahriman.core.database.operations import (
|
||||
AuthOperations,
|
||||
BuildOperations,
|
||||
)
|
||||
```
|
||||
|
||||
* Imports goes in alphabetical order, no relative imports allowed. Same rule applies to frontend classes.
|
||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines. Same rule applies to frontend classes.
|
||||
* It is possible to create file which contains some functions (e.g. `ahriman.core.utils`), but in this case you would need to define `__all__` attribute.
|
||||
* One file should define only one class, exception is class satellites in case if file length remains less than 400 lines.
|
||||
* It is possible to create file which contains some functions (e.g. `ahriman.core.util`), but in this case you would need to define `__all__` attribute.
|
||||
* The file size mentioned above must be applicable in general. In case of big classes consider splitting them into traits. Note, however, that `pylint` includes comments and docstrings into counter, thus you need to check file size by other tools.
|
||||
* No global variable is allowed outside of `ahriman` module. `ahriman.core.context` is also special case.
|
||||
* Single quotes are not allowed. The reason behind this restriction is the fact that docstrings must be written by using double quotes only, and we would like to make style consistent.
|
||||
@@ -221,14 +214,11 @@ Again, the most checks can be performed by `tox` command, though some additional
|
||||
* It is allowed to change web API to add new fields or remove optional ones. However, in case of model changes, new API version must be introduced.
|
||||
* On the other hand, it is allowed to change method signatures, however, it is recommended to add new parameters as optional if possible. Deprecated API can be dropped during major release.
|
||||
* Enumerations (`Enum` classes) are allowed and recommended. However, it is recommended to use `StrEnum` class if there are from/to string conversions and `IntEnum` otherwise.
|
||||
* `Generator` return type is not allowed. Generator functions must return generic `Iterator` object. Documentation should be described as `Yields`, however, because of pylint checks. Unfortunately, `Iterable` return type is not available for generators also, because of specific `contextlib.contextmanager` case.
|
||||
|
||||
### Other checks
|
||||
|
||||
The projects also uses typing checks (provided by `mypy`) and some linter checks provided by `pylint` and `bandit`. Those checks must be passed successfully for any open pull requests.
|
||||
|
||||
Frontend checks normally are performed by `eslint` (e.g. `npx run eslint`).
|
||||
|
||||
## Developers how to
|
||||
|
||||
### Run automated checks
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
[archive]
|
||||
; Keep amount of last built packages in archive. 0 means keep all packages
|
||||
keep_built_packages = 1
|
||||
|
||||
[logs-rotation]
|
||||
; Keep last build logs for each package
|
||||
keep_last_logs = 5
|
||||
@@ -1,68 +0,0 @@
|
||||
[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"
|
||||
@@ -1,20 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
__version__ = "2.20.0"
|
||||
@@ -1,81 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.formatters import PackagePrinter
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Archives(Handler):
|
||||
"""
|
||||
package archives handler
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
application = Application(repository_id, configuration, report=True)
|
||||
|
||||
match args.action:
|
||||
case Action.List:
|
||||
archives = application.repository.package_archives(args.package)
|
||||
for package in archives:
|
||||
PackagePrinter(package, BuildStatus(BuildStatusEnum.Success))(verbose=args.info)
|
||||
|
||||
Archives.check_status(args.exit_code, bool(archives))
|
||||
|
||||
@staticmethod
|
||||
def _set_package_archives_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package archives subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-archives", help="list package archive versions",
|
||||
description="list available archive versions for the package")
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||
action="store_true")
|
||||
parser.add_argument("--info", help="show additional package information",
|
||||
action=argparse.BooleanOptionalAction, default=False)
|
||||
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
arguments = [_set_package_archives_parser]
|
||||
@@ -1,93 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Hold(Handler):
|
||||
"""
|
||||
package hold handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
client = Application(repository_id, configuration, report=True).reporter
|
||||
|
||||
match args.action:
|
||||
case Action.Remove:
|
||||
for package in args.package:
|
||||
client.package_hold_update(package, enabled=False)
|
||||
case Action.Update:
|
||||
for package in args.package:
|
||||
client.package_hold_update(package, enabled=True)
|
||||
|
||||
@staticmethod
|
||||
def _set_package_hold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for hold package subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-hold", help="hold package",
|
||||
description="hold package from automatic updates")
|
||||
parser.add_argument("package", help="package base", nargs="+")
|
||||
parser.set_defaults(action=Action.Update, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def _set_package_unhold_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for unhold package subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-unhold", help="unhold package",
|
||||
description="remove package hold, allowing automatic updates")
|
||||
parser.add_argument("package", help="package base", nargs="+")
|
||||
parser.set_defaults(action=Action.Remove, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
arguments = [
|
||||
_set_package_hold_parser,
|
||||
_set_package_unhold_parser,
|
||||
]
|
||||
@@ -1,100 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from dataclasses import replace
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.formatters import PkgbuildPrinter
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Pkgbuild(Handler):
|
||||
"""
|
||||
package pkgbuild handler
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # conflicting io
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
client = Application(repository_id, configuration, report=True).reporter
|
||||
|
||||
match args.action:
|
||||
case Action.List:
|
||||
changes = client.package_changes_get(args.package)
|
||||
PkgbuildPrinter(changes)(verbose=True, separator="")
|
||||
Pkgbuild.check_status(args.exit_code, changes.pkgbuild is not None)
|
||||
case Action.Remove:
|
||||
changes = client.package_changes_get(args.package)
|
||||
client.package_changes_update(args.package, replace(changes, pkgbuild=None))
|
||||
|
||||
@staticmethod
|
||||
def _set_package_pkgbuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package pkgbuild subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-pkgbuild", help="get package pkgbuild",
|
||||
description="retrieve package PKGBUILD stored in database",
|
||||
epilog="This command requests package status from the web interface "
|
||||
"if it is available.")
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty",
|
||||
action="store_true")
|
||||
parser.set_defaults(action=Action.List, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def _set_package_pkgbuild_remove_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package pkgbuild remove subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-pkgbuild-remove", help="remove package pkgbuild",
|
||||
description="remove the package PKGBUILD stored remotely")
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.set_defaults(action=Action.Remove, exit_code=False, lock=None, quiet=True, report=False, unsafe=True)
|
||||
return parser
|
||||
|
||||
arguments = [_set_package_pkgbuild_parser, _set_package_pkgbuild_remove_parser]
|
||||
@@ -1,69 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Reload(Handler):
|
||||
"""
|
||||
web server reload handler
|
||||
"""
|
||||
|
||||
ALLOW_MULTI_ARCHITECTURE_RUN = False # system-wide action
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
client = Application(repository_id, configuration, report=True).reporter
|
||||
client.configuration_reload()
|
||||
|
||||
@staticmethod
|
||||
def _set_web_reload_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for web reload subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("web-reload", help="reload configuration",
|
||||
description="reload web server configuration",
|
||||
epilog="This method forces the web server to reload its configuration. "
|
||||
"Note, however, that this method does not apply all configuration changes "
|
||||
"(like ports, authentication, etc)")
|
||||
parser.set_defaults(architecture="", lock=None, quiet=True, report=False, repository="", unsafe=True)
|
||||
return parser
|
||||
|
||||
arguments = [_set_web_reload_parser]
|
||||
@@ -1,131 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import argparse
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.application.handlers.handler import Handler, SubParserAction
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.utils import extract_user
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class Rollback(Handler):
|
||||
"""
|
||||
package rollback handler
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def run(cls, args: argparse.Namespace, repository_id: RepositoryId, configuration: Configuration, *,
|
||||
report: bool) -> None:
|
||||
"""
|
||||
callback for command line
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line args
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
report(bool): force enable or disable reporting
|
||||
"""
|
||||
application = Application(repository_id, configuration, report=report)
|
||||
application.on_start()
|
||||
|
||||
package = Rollback.package_load(application, args.package, args.version)
|
||||
artifacts = Rollback.package_artifacts(application, package)
|
||||
|
||||
args.package = [str(artifact) for artifact in artifacts]
|
||||
Add.perform_action(application, args)
|
||||
|
||||
if args.hold:
|
||||
application.reporter.package_hold_update(package.base, enabled=True)
|
||||
|
||||
@staticmethod
|
||||
def _set_package_rollback_parser(root: SubParserAction) -> argparse.ArgumentParser:
|
||||
"""
|
||||
add parser for package rollback subcommand
|
||||
|
||||
Args:
|
||||
root(SubParserAction): subparsers for the commands
|
||||
|
||||
Returns:
|
||||
argparse.ArgumentParser: created argument parser
|
||||
"""
|
||||
parser = root.add_parser("package-rollback", help="rollback package",
|
||||
description="rollback package to specified version from archives")
|
||||
parser.add_argument("package", help="package base")
|
||||
parser.add_argument("version", help="package version")
|
||||
parser.add_argument("--hold", help="hold package afterwards",
|
||||
action=argparse.BooleanOptionalAction, default=True)
|
||||
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,
|
||||
exit_code=True, increment=False, now=True, local=False, manual=False, refresh=False,
|
||||
source=PackageSource.Archive, variable=None, vcs=False)
|
||||
return parser
|
||||
|
||||
@staticmethod
|
||||
def package_artifacts(application: Application, package: Package) -> list[Path]:
|
||||
"""
|
||||
look for requested package artifacts and return paths to them
|
||||
|
||||
Args:
|
||||
application(Application): application instance
|
||||
package(Package): package descriptor
|
||||
|
||||
Returns:
|
||||
list[Path]: paths to found artifacts
|
||||
|
||||
Raises:
|
||||
UnknownPackageError: if artifacts do not exist
|
||||
"""
|
||||
# lookup for built artifacts
|
||||
artifacts = application.repository.package_archives_lookup(package)
|
||||
if not artifacts:
|
||||
raise UnknownPackageError(package.base)
|
||||
return artifacts
|
||||
|
||||
@staticmethod
|
||||
def package_load(application: Application, package_base: str, version: str) -> Package:
|
||||
"""
|
||||
load package from repository, while setting requested version
|
||||
|
||||
Args:
|
||||
application(Application): application instance
|
||||
package_base(str): package base
|
||||
version(str): package version
|
||||
|
||||
Returns:
|
||||
Package: loaded package
|
||||
|
||||
Raises:
|
||||
UnknownPackageError: if package does not exist
|
||||
"""
|
||||
try:
|
||||
package, _ = next(iter(application.reporter.package_get(package_base)))
|
||||
return replace(package, version=version)
|
||||
except StopIteration:
|
||||
raise UnknownPackageError(package_base) from None
|
||||
|
||||
arguments = [_set_package_rollback_parser]
|
||||
@@ -1,81 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 pathlib import Path
|
||||
from pyalpm import Handle, Package # type: ignore[import-not-found]
|
||||
from tempfile import TemporaryDirectory
|
||||
from typing import Any, ClassVar, Self
|
||||
|
||||
|
||||
class PacmanHandle:
|
||||
"""
|
||||
lightweight wrapper for pacman handle to be used for direct alpm operations (e.g. package load)
|
||||
|
||||
Attributes:
|
||||
handle(Handle): pyalpm handle instance
|
||||
"""
|
||||
|
||||
_ephemeral: ClassVar[Self | None] = None
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
Args:
|
||||
*args(Any): positional arguments for :class:`pyalpm.Handle`
|
||||
**kwargs(Any): keyword arguments for :class:`pyalpm.Handle`
|
||||
"""
|
||||
self.handle = Handle(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def ephemeral(cls) -> Self:
|
||||
"""
|
||||
create temporary instance with no access to real databases
|
||||
|
||||
Returns:
|
||||
Self: loaded class
|
||||
"""
|
||||
if cls._ephemeral is None:
|
||||
# handle creates alpm version file, but we don't use it
|
||||
# so it is ok to just remove it
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||
cls._ephemeral = cls("/", dir_name)
|
||||
return cls._ephemeral
|
||||
|
||||
def package_load(self, path: Path) -> Package:
|
||||
"""
|
||||
load package from path to the archive
|
||||
|
||||
Args:
|
||||
path(Path): path to package archive
|
||||
|
||||
Returns:
|
||||
Package: package instance
|
||||
"""
|
||||
return self.handle.load_pkg(str(path))
|
||||
|
||||
def __getattr__(self, item: str) -> Any:
|
||||
"""
|
||||
proxy methods for :class:`pyalpm.Handle`, because it doesn't allow subclassing
|
||||
|
||||
Args:
|
||||
item(str): property name
|
||||
|
||||
Returns:
|
||||
Any: attribute by its name
|
||||
"""
|
||||
return self.handle.__getattribute__(item)
|
||||
@@ -1,114 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import full_version, utcnow
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
|
||||
|
||||
class PackageVersion(LazyLogging):
|
||||
"""
|
||||
package version extractor and helper
|
||||
|
||||
Attributes:
|
||||
package(Package): package definitions
|
||||
"""
|
||||
|
||||
def __init__(self, package: Package) -> None:
|
||||
"""
|
||||
Args:
|
||||
package(Package): package definitions
|
||||
"""
|
||||
self.package = package
|
||||
|
||||
def actual_version(self, configuration: Configuration) -> str:
|
||||
"""
|
||||
additional method to handle VCS package versions
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
str: package version if package is not VCS and current version according to VCS otherwise
|
||||
"""
|
||||
if not self.package.is_vcs:
|
||||
return self.package.version
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
paths = configuration.repository_paths
|
||||
task = Task(self.package, configuration, repository_id.architecture, paths)
|
||||
|
||||
try:
|
||||
# create fresh chroot environment, fetch sources and - automagically - update PKGBUILD
|
||||
task.init(paths.cache_for(self.package.base), [], None)
|
||||
pkgbuild = Pkgbuild.from_file(paths.cache_for(self.package.base) / "PKGBUILD")
|
||||
|
||||
return full_version(pkgbuild.get("epoch"), pkgbuild["pkgver"], pkgbuild["pkgrel"])
|
||||
except Exception:
|
||||
self.logger.exception("cannot determine version of VCS package")
|
||||
finally:
|
||||
# clear log files generated by devtools
|
||||
for log_file in paths.cache_for(self.package.base).glob("*.log"):
|
||||
log_file.unlink()
|
||||
|
||||
return self.package.version
|
||||
|
||||
def is_newer_than(self, timestamp: float | int) -> bool:
|
||||
"""
|
||||
check if package was built after the specified timestamp
|
||||
|
||||
Args:
|
||||
timestamp(float | int): timestamp to check build date against
|
||||
|
||||
Returns:
|
||||
bool: ``True`` in case if package was built after the specified date and ``False`` otherwise.
|
||||
In case if build date is not set by any of packages, it returns False
|
||||
"""
|
||||
return any(
|
||||
package.build_date > timestamp
|
||||
for package in self.package.packages.values()
|
||||
if package.build_date is not None
|
||||
)
|
||||
|
||||
def is_outdated(self, remote: Package, configuration: Configuration, *,
|
||||
calculate_version: bool = True) -> bool:
|
||||
"""
|
||||
check if package is out-of-dated
|
||||
|
||||
Args:
|
||||
remote(Package): package properties from remote source
|
||||
configuration(Configuration): configuration instance
|
||||
calculate_version(bool, optional): expand version to actual value (by calculating git versions)
|
||||
(Default value = True)
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the package is out-of-dated and ``False`` otherwise
|
||||
"""
|
||||
vcs_allowed_age = configuration.getint("build", "vcs_allowed_age", fallback=0)
|
||||
min_vcs_build_date = utcnow().timestamp() - vcs_allowed_age
|
||||
|
||||
if calculate_version and not self.is_newer_than(min_vcs_build_date):
|
||||
remote_version = PackageVersion(remote).actual_version(configuration)
|
||||
else:
|
||||
remote_version = remote.version
|
||||
|
||||
return self.package.vercmp(remote_version) < 0
|
||||
@@ -1,30 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
__all__ = ["steps"]
|
||||
|
||||
|
||||
steps = [
|
||||
"""
|
||||
alter table logs add column process_id text not null default ''
|
||||
""",
|
||||
"""
|
||||
alter table logs rename column record to message
|
||||
""",
|
||||
]
|
||||
@@ -1,77 +0,0 @@
|
||||
#
|
||||
# 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 dataclasses import replace
|
||||
from sqlite3 import Connection
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Explorer
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.utils import atomic_move, package_like, symlink_relative
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
__all__ = ["migrate_data"]
|
||||
|
||||
|
||||
def migrate_data(connection: Connection, configuration: Configuration) -> None:
|
||||
"""
|
||||
perform data migration
|
||||
|
||||
Args:
|
||||
connection(Connection): database connection
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
del connection
|
||||
|
||||
for repository_id in Explorer.repositories_extract(configuration):
|
||||
paths = replace(configuration.repository_paths, repository_id=repository_id)
|
||||
|
||||
# create archive directory if required
|
||||
if not paths.archive.is_dir():
|
||||
with paths.preserve_owner():
|
||||
paths.archive.mkdir(mode=0o755, parents=True)
|
||||
|
||||
move_packages(paths)
|
||||
|
||||
|
||||
def move_packages(repository_paths: RepositoryPaths) -> None:
|
||||
"""
|
||||
move packages from repository to archive and create symbolic links
|
||||
|
||||
Args:
|
||||
repository_paths(RepositoryPaths): repository paths instance
|
||||
"""
|
||||
for archive in filter(package_like, repository_paths.repository.iterdir()):
|
||||
if not archive.is_file(follow_symlinks=False):
|
||||
continue # skip symbolic links if any
|
||||
|
||||
package = Package.from_archive(archive)
|
||||
artifacts = [archive]
|
||||
# check if there are signatures for this package and append it here too
|
||||
if (signature := GPG.signature(archive)).exists():
|
||||
artifacts.append(signature)
|
||||
|
||||
for source in artifacts:
|
||||
target = repository_paths.ensure_exists(repository_paths.archive_for(package.base)) / source.name
|
||||
# move package to the archive directory
|
||||
atomic_move(source, target)
|
||||
# create symlink to the archive
|
||||
symlink_relative(source, target)
|
||||
@@ -1,25 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
__all__ = ["steps"]
|
||||
|
||||
|
||||
steps = [
|
||||
"""alter table package_changes add column pkgbuild text""",
|
||||
]
|
||||
@@ -1,25 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
__all__ = ["steps"]
|
||||
|
||||
|
||||
steps = [
|
||||
"""alter table package_statuses add column is_held integer not null default 0""",
|
||||
]
|
||||
@@ -1,183 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 sqlite3 import Connection
|
||||
|
||||
from ahriman.core.database.operations.operations import Operations
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
class LogsOperations(Operations):
|
||||
"""
|
||||
logs operations
|
||||
"""
|
||||
|
||||
def logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
|
||||
limit: int = -1, offset: int = 0, repository_id: RepositoryId | None = None) -> list[LogRecord]:
|
||||
"""
|
||||
extract logs for specified package base
|
||||
|
||||
Args:
|
||||
package_base(str): package base to extract logs
|
||||
version(str | None, optional): package version to filter (Default value = None)
|
||||
process_id(str | None, optional): process identifier to filter (Default value = None)
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
|
||||
Return:
|
||||
list[LogRecord]: sorted package log records
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> list[LogRecord]:
|
||||
return [
|
||||
LogRecord.from_json(package_base, row)
|
||||
for row in connection.execute(
|
||||
"""
|
||||
select created, message, version, process_id from (
|
||||
select * from logs
|
||||
where package_base = :package_base
|
||||
and repository = :repository
|
||||
and (:version is null or version = :version)
|
||||
and (:process_id is null or process_id = :process_id)
|
||||
order by created desc limit :limit offset :offset
|
||||
) order by created asc
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"version": version,
|
||||
"process_id": process_id,
|
||||
"repository": repository_id.id,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
]
|
||||
|
||||
return self.with_connection(run)
|
||||
|
||||
def logs_insert(self, log_record: LogRecord, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
write new log record to database
|
||||
|
||||
Args:
|
||||
log_record(LogRecord): log record object
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
insert into logs
|
||||
(package_base, version, created, message, repository, process_id)
|
||||
values
|
||||
(:package_base, :version, :created, :message, :repository, :process_id)
|
||||
""",
|
||||
{
|
||||
"package_base": log_record.log_record_id.package_base,
|
||||
"repository": repository_id.id,
|
||||
} | log_record.view()
|
||||
)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def logs_remove(self, package_base: str, version: str | None, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
remove log records for the specified package
|
||||
|
||||
Args:
|
||||
package_base(str): package base to remove logs
|
||||
version(str | None): package version. If set it will remove only logs belonging to another version
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
delete from logs
|
||||
where package_base = :package_base
|
||||
and repository = :repository
|
||||
and (:version is null or version <> :version)
|
||||
""",
|
||||
{
|
||||
"package_base": package_base,
|
||||
"version": version,
|
||||
"repository": repository_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
|
||||
def logs_rotate(self, keep_last_records: int, repository_id: RepositoryId | None = None) -> None:
|
||||
"""
|
||||
rotate logs in storage. This method will remove old logs, keeping only the last N records for each package
|
||||
|
||||
Args:
|
||||
keep_last_records(int): number of last records to keep
|
||||
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
|
||||
"""
|
||||
repository_id = repository_id or self._repository_id
|
||||
|
||||
def remove_duplicates(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
delete from logs
|
||||
where repository = :repository
|
||||
and (package_base, version, repository, process_id) not in (
|
||||
select package_base, version, repository, process_id from logs
|
||||
where (package_base, version, repository, created) in (
|
||||
select package_base, version, repository, max(created) from logs
|
||||
where repository = :repository
|
||||
group by package_base, version, repository
|
||||
)
|
||||
)
|
||||
""",
|
||||
{
|
||||
"repository": repository_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
def remove_older(connection: Connection) -> None:
|
||||
connection.execute(
|
||||
"""
|
||||
delete from logs
|
||||
where (package_base, repository, process_id) in (
|
||||
select package_base, repository, process_id from (
|
||||
select package_base, repository, process_id, row_number() over (partition by package_base order by max(created) desc) as rn
|
||||
from logs
|
||||
where repository = :repository
|
||||
group by package_base, repository, process_id
|
||||
)
|
||||
where rn > :offset
|
||||
)
|
||||
""",
|
||||
{
|
||||
"offset": keep_last_records,
|
||||
"repository": repository_id.id,
|
||||
}
|
||||
)
|
||||
|
||||
def run(connection: Connection) -> None:
|
||||
remove_duplicates(connection)
|
||||
remove_older(connection)
|
||||
|
||||
return self.with_connection(run, commit=True)
|
||||
@@ -1,62 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.core.formatters.printer import Printer
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.property import Property
|
||||
|
||||
|
||||
class PkgbuildPrinter(Printer):
|
||||
"""
|
||||
print content of the pkgbuild stored in changes
|
||||
|
||||
Attributes:
|
||||
changes(Changes): package changes
|
||||
"""
|
||||
|
||||
def __init__(self, changes: Changes) -> None:
|
||||
"""
|
||||
Args:
|
||||
changes(Changes): package changes
|
||||
"""
|
||||
Printer.__init__(self)
|
||||
self.changes = changes
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
|
||||
Returns:
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
if self.changes.pkgbuild is None:
|
||||
return []
|
||||
return [Property("", self.changes.pkgbuild, is_required=True, indent=0)]
|
||||
|
||||
# pylint: disable=redundant-returns-doc
|
||||
def title(self) -> str | None:
|
||||
"""
|
||||
generate entry title from content
|
||||
|
||||
Returns:
|
||||
str | None: content title if it can be generated and ``None`` otherwise
|
||||
"""
|
||||
if self.changes.pkgbuild is None:
|
||||
return None
|
||||
return self.changes.last_commit_sha
|
||||
@@ -1,53 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.core.formatters.string_printer import StringPrinter
|
||||
from ahriman.core.utils import pretty_size
|
||||
from ahriman.models.property import Property
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
class RepositoryStatsPrinter(StringPrinter):
|
||||
"""
|
||||
print repository statistics
|
||||
|
||||
Attributes:
|
||||
statistics(RepositoryStats): repository statistics
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, statistics: RepositoryStats) -> None:
|
||||
"""
|
||||
Args:
|
||||
statistics(RepositoryStats): repository statistics
|
||||
"""
|
||||
StringPrinter.__init__(self, str(repository_id))
|
||||
self.statistics = statistics
|
||||
|
||||
def properties(self) -> list[Property]:
|
||||
"""
|
||||
convert content into printable data
|
||||
|
||||
Returns:
|
||||
list[Property]: list of content properties
|
||||
"""
|
||||
return [
|
||||
Property("Packages", self.statistics.bases),
|
||||
Property("Repository size", pretty_size(self.statistics.archive_size)),
|
||||
]
|
||||
@@ -1,21 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.core.housekeeping.archive_rotation_trigger import ArchiveRotationTrigger
|
||||
from ahriman.core.housekeeping.logs_rotation_trigger import LogsRotationTrigger
|
||||
@@ -1,110 +0,0 @@
|
||||
#
|
||||
# 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 ahriman.core import context
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class ArchiveRotationTrigger(Trigger):
|
||||
"""
|
||||
remove packages from archive
|
||||
|
||||
Attributes:
|
||||
keep_built_packages(int): number of last packages to keep
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"archive": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"keep_built_packages": {
|
||||
"type": "integer",
|
||||
"required": True,
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
|
||||
section = next(iter(self.configuration_sections(configuration)))
|
||||
self.keep_built_packages = max(configuration.getint(section, "keep_built_packages"), 0)
|
||||
self.paths = configuration.repository_paths
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls, configuration: Configuration) -> list[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
list[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return list(cls.CONFIGURATION_SCHEMA.keys())
|
||||
|
||||
def archives_remove(self, package: Package, repository: Repository) -> None:
|
||||
"""
|
||||
remove older versions of the specified package
|
||||
|
||||
Args:
|
||||
package(Package): package which has been updated to check for older versions
|
||||
repository(Repository): repository instance
|
||||
"""
|
||||
# explicit guard to skip process in case if rotation is disabled
|
||||
# this guard is supposed to speedup process
|
||||
if self.keep_built_packages == 0:
|
||||
return
|
||||
|
||||
to_remove = repository.package_archives(package.base)
|
||||
|
||||
for single in to_remove[:-self.keep_built_packages]:
|
||||
self.logger.info("removing version %s of package %s", single.version, single.base)
|
||||
for archive in single.packages.values():
|
||||
for path in self.paths.archive_for(single.base).glob(f"{archive.filename}*"):
|
||||
path.unlink()
|
||||
|
||||
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||
"""
|
||||
run trigger
|
||||
|
||||
Args:
|
||||
result(Result): build result
|
||||
packages(list[Package]): list of all available packages
|
||||
"""
|
||||
ctx = context.get()
|
||||
repository = ctx.get(Repository)
|
||||
|
||||
for package in result.success:
|
||||
self.archives_remove(package, repository)
|
||||
@@ -1,87 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.core import context
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class LogsRotationTrigger(Trigger):
|
||||
"""
|
||||
rotate logs after build processes
|
||||
|
||||
Attributes:
|
||||
keep_last_records(int): number of last records to keep
|
||||
"""
|
||||
|
||||
CONFIGURATION_SCHEMA = {
|
||||
"logs-rotation": {
|
||||
"type": "dict",
|
||||
"schema": {
|
||||
"keep_last_logs": {
|
||||
"type": "integer",
|
||||
"required": True,
|
||||
"coerce": "integer",
|
||||
"min": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
|
||||
section = next(iter(self.configuration_sections(configuration)))
|
||||
self.keep_last_records = configuration.getint( # read old-style first and then fallback to new style
|
||||
"settings", "keep_last_logs",
|
||||
fallback=configuration.getint(section, "keep_last_logs"))
|
||||
|
||||
@classmethod
|
||||
def configuration_sections(cls, configuration: Configuration) -> list[str]:
|
||||
"""
|
||||
extract configuration sections from configuration
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
|
||||
Returns:
|
||||
list[str]: read configuration sections belong to this trigger
|
||||
"""
|
||||
return list(cls.CONFIGURATION_SCHEMA.keys())
|
||||
|
||||
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||
"""
|
||||
run trigger
|
||||
|
||||
Args:
|
||||
result(Result): build result
|
||||
packages(list[Package]): list of all available packages
|
||||
"""
|
||||
ctx = context.get()
|
||||
reporter = ctx.get(Client)
|
||||
reporter.logs_rotate(self.keep_last_records)
|
||||
@@ -1,108 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import contextvars
|
||||
import logging
|
||||
|
||||
from typing import Any, ClassVar, TypeVar, cast
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class LogContext:
|
||||
"""
|
||||
logging context manager which provides context variables injection into log records
|
||||
"""
|
||||
|
||||
_context: ClassVar[dict[str, contextvars.ContextVar[Any]]] = {}
|
||||
|
||||
@classmethod
|
||||
def get(cls, name: str) -> T | None:
|
||||
"""
|
||||
get context variable if available
|
||||
|
||||
Args:
|
||||
name(str): name of the context variable
|
||||
|
||||
Returns:
|
||||
T | None: context variable if available and ``None`` otherwise
|
||||
"""
|
||||
if (variable := cls._context.get(name)) is not None:
|
||||
return cast(T | None, variable.get())
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def log_record_factory(cls, *args: Any, **kwargs: Any) -> logging.LogRecord:
|
||||
"""
|
||||
log record factory which injects all registered context variables into log records
|
||||
|
||||
Args:
|
||||
*args(Any): positional arguments for the log factory
|
||||
**kwargs(Any): keyword arguments for the log factory
|
||||
|
||||
Returns:
|
||||
logging.LogRecord: log record with context variables set as attributes
|
||||
"""
|
||||
record = logging.LogRecord(*args, **kwargs)
|
||||
|
||||
for name, variable in cls._context.items():
|
||||
if (value := variable.get()) is not None:
|
||||
setattr(record, name, value)
|
||||
|
||||
return record
|
||||
|
||||
@classmethod
|
||||
def register(cls, name: str) -> contextvars.ContextVar[T]:
|
||||
"""
|
||||
(re)create context variable for log records
|
||||
|
||||
Args:
|
||||
name(str): name of the context variable
|
||||
|
||||
Returns:
|
||||
contextvars.ContextVar[T]: created context variable
|
||||
"""
|
||||
variable = cls._context[name] = contextvars.ContextVar(name, default=None)
|
||||
return variable
|
||||
|
||||
@classmethod
|
||||
def reset(cls, name: str, token: contextvars.Token[T]) -> None:
|
||||
"""
|
||||
reset context variable to its previous value
|
||||
|
||||
Args:
|
||||
name(str): attribute name to reset on log records
|
||||
token(contextvars.Token[T]): previously registered token
|
||||
"""
|
||||
cls._context[name].reset(token)
|
||||
|
||||
@classmethod
|
||||
def set(cls, name: str, value: T) -> contextvars.Token[T]:
|
||||
"""
|
||||
set context variable for log records. This value will be automatically emitted with each log record
|
||||
|
||||
Args:
|
||||
name(str): attribute name to set on log records
|
||||
value(T): current value of the context variable
|
||||
|
||||
Returns:
|
||||
contextvars.Token[T]: token created with this value
|
||||
"""
|
||||
return cls._context[name].set(value)
|
||||
@@ -1,70 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 collections.abc import Iterable
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class Explorer:
|
||||
"""
|
||||
helper to read filesystem and find created repositories
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def repositories_extract(configuration: Configuration, repository: str | None = None,
|
||||
architecture: str | None = None) -> list[RepositoryId]:
|
||||
"""
|
||||
get known architectures
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration instance
|
||||
repository(str | None, optional): predefined repository name if available (Default value = None)
|
||||
architecture(str | None, optional): predefined repository architecture if available (Default value = None)
|
||||
|
||||
Returns:
|
||||
list[RepositoryId]: list of repository names and architectures for which tree is created
|
||||
"""
|
||||
# pylint, wtf???
|
||||
root = configuration.getpath("repository", "root") # pylint: disable=assignment-from-no-return
|
||||
|
||||
# extract repository names first
|
||||
if repository is not None:
|
||||
repositories: Iterable[str] = [repository]
|
||||
elif from_filesystem := RepositoryPaths.known_repositories(root):
|
||||
repositories = from_filesystem
|
||||
else: # try to read configuration now
|
||||
repositories = [configuration.get("repository", "name")]
|
||||
|
||||
# extract architecture names
|
||||
if architecture is not None:
|
||||
parsed = set(
|
||||
RepositoryId(architecture, repository)
|
||||
for repository in repositories
|
||||
)
|
||||
else: # try to read from file system
|
||||
parsed = set(
|
||||
RepositoryId(architecture, repository)
|
||||
for repository in repositories
|
||||
for architecture in RepositoryPaths.known_architectures(root, repository)
|
||||
)
|
||||
|
||||
return sorted(parsed)
|
||||
@@ -1,239 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import copy
|
||||
|
||||
from collections.abc import Callable, Iterable
|
||||
from functools import cmp_to_key
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.build_tools.sources import Sources
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.utils import list_flatmap, package_like
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class PackageInfo(LazyLogging):
|
||||
"""
|
||||
handler for the package information
|
||||
|
||||
Attributes:
|
||||
configuration(Configuration): configuration instance
|
||||
pacman(Pacman): alpm wrapper instance
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
reporter(Client): build status reporter instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
"""
|
||||
|
||||
configuration: Configuration
|
||||
pacman: Pacman
|
||||
paths: RepositoryPaths
|
||||
reporter: Client
|
||||
repository_id: RepositoryId
|
||||
|
||||
def full_depends(self, package: Package, packages: Iterable[Package]) -> list[str]:
|
||||
"""
|
||||
generate full dependencies list including transitive dependencies
|
||||
|
||||
Args:
|
||||
package(Package): package to check dependencies for
|
||||
packages(Iterable[Package]): repository package list
|
||||
|
||||
Returns:
|
||||
list[str]: all dependencies of the package
|
||||
"""
|
||||
dependencies = {}
|
||||
# load own package dependencies
|
||||
for package_base in packages:
|
||||
for name, repo_package in package_base.packages.items():
|
||||
dependencies[name] = repo_package.depends
|
||||
for provides in repo_package.provides:
|
||||
dependencies[provides] = repo_package.depends
|
||||
# load repository dependencies
|
||||
for database in self.pacman.handle.get_syncdbs():
|
||||
for pacman_package in database.pkgcache:
|
||||
dependencies[pacman_package.name] = pacman_package.depends
|
||||
for provides in pacman_package.provides:
|
||||
dependencies[provides] = pacman_package.depends
|
||||
|
||||
result = set(package.depends)
|
||||
current_depends: set[str] = set()
|
||||
while result != current_depends:
|
||||
current_depends = copy.deepcopy(result)
|
||||
for package_name in current_depends:
|
||||
result.update(dependencies.get(package_name, []))
|
||||
|
||||
return sorted(result)
|
||||
|
||||
def load_archives(self, packages: Iterable[Path], *, latest_only: bool = True) -> list[Package]:
|
||||
"""
|
||||
load packages from list of archives
|
||||
|
||||
Args:
|
||||
packages(Iterable[Path]): paths to package archives
|
||||
latest_only(bool, optional): filter packages with the same base, keeping only fresh packages installed
|
||||
(Default value = True)
|
||||
|
||||
Returns:
|
||||
list[Package]: list of read packages
|
||||
"""
|
||||
sources = {package.base: package.remote for package, _, in self.reporter.package_get(None)}
|
||||
|
||||
result: dict[str, dict[str, Package]] = {}
|
||||
# we are iterating over bases, not single packages
|
||||
for full_path in packages:
|
||||
try:
|
||||
local = Package.from_archive(full_path)
|
||||
if (source := sources.get(local.base)) is not None: # update source with remote
|
||||
local.remote = source
|
||||
|
||||
loaded_versions = result.setdefault(local.base, {})
|
||||
current = loaded_versions.setdefault(local.version, local)
|
||||
current.packages.update(local.packages)
|
||||
except Exception:
|
||||
self.logger.exception("could not load package from %s", full_path)
|
||||
|
||||
if latest_only:
|
||||
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
||||
for package_base, versions in result.items():
|
||||
newest = max(versions.values(), key=cmp_to_key(comparator))
|
||||
result[package_base] = {newest.version: newest}
|
||||
|
||||
return [
|
||||
package
|
||||
for versions in result.values()
|
||||
for package in versions.values()
|
||||
]
|
||||
|
||||
def package_archives(self, package_base: str) -> list[Package]:
|
||||
"""
|
||||
load list of packages known for this package base. This method unlike
|
||||
:func:`ahriman.core.repository.package_info.PackageInfo.load_archives` scans archive directory and loads all
|
||||
versions available for the ``package_base``
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages belonging to this base, sorted by version by ascension
|
||||
"""
|
||||
archive = self.paths.archive_for(package_base)
|
||||
if not archive.is_dir():
|
||||
return []
|
||||
|
||||
packages = self.load_archives(filter(package_like, archive.iterdir()), latest_only=False)
|
||||
|
||||
comparator: Callable[[Package, Package], int] = lambda left, right: left.vercmp(right.version)
|
||||
return sorted(
|
||||
(package for package in packages if package.supports_architecture(self.repository_id.architecture)),
|
||||
key=cmp_to_key(comparator),
|
||||
)
|
||||
|
||||
def package_archives_lookup(self, package: Package) -> list[Path]:
|
||||
"""
|
||||
check if there is a rebuilt package already
|
||||
|
||||
Args:
|
||||
package(Package): package to check
|
||||
|
||||
Returns:
|
||||
list[Path]: list of built packages and signatures if available, empty list otherwise
|
||||
"""
|
||||
archive = self.paths.archive_for(package.base)
|
||||
|
||||
for built in self.package_archives(package.base):
|
||||
if built.version != package.version:
|
||||
continue
|
||||
|
||||
return list_flatmap(built.packages.values(), lambda single: archive.glob(f"{single.filename}*"))
|
||||
|
||||
return []
|
||||
|
||||
def package_changes(self, package: Package, last_commit_sha: str) -> Changes | None:
|
||||
"""
|
||||
extract package change for the package since last commit if available
|
||||
|
||||
Args:
|
||||
package(Package): package properties
|
||||
last_commit_sha(str): last known commit hash
|
||||
|
||||
Returns:
|
||||
Changes | None: changes if available
|
||||
"""
|
||||
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
|
||||
dir_path = Path(dir_name)
|
||||
patches = self.reporter.package_patches_get(package.base, None)
|
||||
current_commit_sha = Sources.load(dir_path, package, patches, self.paths)
|
||||
|
||||
if current_commit_sha != last_commit_sha:
|
||||
return Sources.changes(dir_path, last_commit_sha)
|
||||
return None
|
||||
|
||||
def packages(self, filter_packages: Iterable[str] | None = None) -> list[Package]:
|
||||
"""
|
||||
generate list of repository packages
|
||||
|
||||
Args:
|
||||
filter_packages(Iterable[str] | None, optional): filter packages list by specified only
|
||||
|
||||
Returns:
|
||||
list[Package]: list of packages properties
|
||||
"""
|
||||
packages = self.load_archives(filter(package_like, self.paths.repository.iterdir()))
|
||||
if filter_packages:
|
||||
packages = [package for package in packages if package.base in filter_packages]
|
||||
|
||||
return packages
|
||||
|
||||
def packages_built(self) -> list[Path]:
|
||||
"""
|
||||
get list of files in built packages directory
|
||||
|
||||
Returns:
|
||||
list[Path]: list of filenames from the directory
|
||||
"""
|
||||
return list(filter(package_like, self.paths.packages.iterdir()))
|
||||
|
||||
def packages_depend_on(self, packages: list[Package], depends_on: Iterable[str] | None) -> list[Package]:
|
||||
"""
|
||||
extract list of packages which depends on specified package
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to be filtered
|
||||
depends_on(Iterable[str] | None): dependencies of the packages
|
||||
|
||||
Returns:
|
||||
list[Package]: list of repository packages which depend on specified packages
|
||||
"""
|
||||
if depends_on is None:
|
||||
return packages # no list provided extract everything by default
|
||||
depends_on = set(depends_on)
|
||||
|
||||
return [
|
||||
package
|
||||
for package in packages
|
||||
if depends_on.intersection(self.full_depends(package, packages))
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
@@ -1,136 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import uuid
|
||||
|
||||
from asyncio import Lock, Queue, QueueFull, QueueShutDown
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.models.event import EventType
|
||||
|
||||
|
||||
SSEvent = tuple[str, dict[str, Any]]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Subscription:
|
||||
"""
|
||||
internal event bus subscription record
|
||||
|
||||
Attributes:
|
||||
topics(list[EventType] | None): event type filter, ``None`` means all
|
||||
object_id(str | None): object identifier filter, ``None`` means all
|
||||
queue(Queue[SSEvent]): per-subscriber event queue
|
||||
"""
|
||||
|
||||
topics: list[EventType] | None
|
||||
object_id: str | None
|
||||
queue: Queue[SSEvent]
|
||||
|
||||
|
||||
class EventBus(LazyLogging):
|
||||
"""
|
||||
event bus implementation
|
||||
|
||||
Attributes:
|
||||
max_size(int): maximum size of queue
|
||||
"""
|
||||
|
||||
def __init__(self, max_size: int) -> None:
|
||||
"""
|
||||
Args:
|
||||
max_size(int): maximum size of queue
|
||||
"""
|
||||
self.max_size = max_size
|
||||
|
||||
self._lock = Lock()
|
||||
self._subscribers: dict[str, _Subscription] = {}
|
||||
|
||||
async def broadcast(self, event_type: EventType, object_id: str | None, **kwargs: Any) -> None:
|
||||
"""
|
||||
broadcast event to all subscribers
|
||||
|
||||
Args:
|
||||
event_type(EventType): event type
|
||||
object_id(str | None): object identifier (e.g. package base)
|
||||
**kwargs(Any): additional event data
|
||||
"""
|
||||
event: dict[str, Any] = {"object_id": object_id}
|
||||
event.update(kwargs)
|
||||
|
||||
async with self._lock:
|
||||
snapshot = list(self._subscribers.items())
|
||||
|
||||
for subscriber_id, subscription in snapshot:
|
||||
if subscription.topics is not None and event_type not in subscription.topics:
|
||||
continue
|
||||
if subscription.object_id is not None and object_id != subscription.object_id:
|
||||
continue
|
||||
|
||||
try:
|
||||
subscription.queue.put_nowait((event_type, event))
|
||||
except QueueFull:
|
||||
self.logger.warning("discard message to slow subscriber %s", subscriber_id)
|
||||
except QueueShutDown:
|
||||
pass
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
gracefully shutdown all subscribers
|
||||
"""
|
||||
async with self._lock:
|
||||
for subscription in self._subscribers.values():
|
||||
subscription.queue.shutdown()
|
||||
self._subscribers.clear()
|
||||
|
||||
async def subscribe(self, topics: list[EventType] | None = None,
|
||||
object_id: str | None = None) -> tuple[str, Queue[SSEvent]]:
|
||||
"""
|
||||
register new subscriber
|
||||
|
||||
Args:
|
||||
topics(list[EventType] | None, optional): list of event types to filter by. If ``None`` is set,
|
||||
all events will be delivered (Default value = None)
|
||||
object_id(str | None, optional): object identifier to filter by. If ``None`` is set,
|
||||
events for all objects will be delivered (Default value = None)
|
||||
|
||||
Returns:
|
||||
tuple[str, Queue[SSEvent]]: subscriber identifier and associated queue
|
||||
"""
|
||||
subscriber_id = str(uuid.uuid4())
|
||||
queue: Queue[SSEvent] = Queue(self.max_size)
|
||||
|
||||
async with self._lock:
|
||||
self._subscribers[subscriber_id] = _Subscription(topics=topics, object_id=object_id, queue=queue)
|
||||
|
||||
return subscriber_id, queue
|
||||
|
||||
async def unsubscribe(self, subscriber_id: str) -> None:
|
||||
"""
|
||||
unsubscribe from events
|
||||
|
||||
Args:
|
||||
subscriber_id(str): subscriber unique identifier
|
||||
"""
|
||||
async with self._lock:
|
||||
subscription = self._subscribers.pop(subscriber_id, None)
|
||||
if subscription is not None:
|
||||
subscription.queue.shutdown()
|
||||
@@ -1,361 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
# pylint: disable=too-many-public-methods
|
||||
from asyncio import Lock
|
||||
from dataclasses import replace
|
||||
from typing import Self
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.repository.package_info import PackageInfo
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.core.status.event_bus import EventBus
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
class Watcher(LazyLogging):
|
||||
"""
|
||||
package status watcher
|
||||
|
||||
Attributes:
|
||||
client(Client): reporter instance
|
||||
event_bus(EventBus): event bus instance
|
||||
package_info(PackageInfo): package info instance
|
||||
status(BuildStatus): daemon status
|
||||
"""
|
||||
|
||||
def __init__(self, client: Client, package_info: PackageInfo, event_bus: EventBus) -> None:
|
||||
"""
|
||||
Args:
|
||||
client(Client): reporter instance
|
||||
package_info(PackageInfo): package info instance
|
||||
event_bus(EventBus): event bus instance
|
||||
"""
|
||||
self.client = client
|
||||
self.package_info = package_info
|
||||
self.event_bus = event_bus
|
||||
|
||||
self._lock = Lock()
|
||||
self._known: dict[str, tuple[Package, BuildStatus]] = {}
|
||||
self.status = BuildStatus()
|
||||
|
||||
async def event_add(self, event: Event) -> None:
|
||||
"""
|
||||
create new event
|
||||
|
||||
Args:
|
||||
event(Event): audit log event
|
||||
"""
|
||||
self.client.event_add(event)
|
||||
|
||||
async def event_get(self, event: str | EventType | None, object_id: str | None,
|
||||
from_date: int | float | None = None, to_date: int | float | None = None,
|
||||
limit: int = -1, offset: int = 0) -> list[Event]:
|
||||
"""
|
||||
retrieve list of events
|
||||
|
||||
Args:
|
||||
event(str | EventType | None): filter by event type
|
||||
object_id(str | None): filter by event object
|
||||
from_date(int | float | None, optional): minimal creation date, inclusive (Default value = None)
|
||||
to_date(int | float | None, optional): maximal creation date, exclusive (Default value = None)
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[Event]: list of audit log events
|
||||
"""
|
||||
return self.client.event_get(event, object_id, from_date, to_date, limit, offset)
|
||||
|
||||
async def load(self) -> None:
|
||||
"""
|
||||
load packages from local database
|
||||
"""
|
||||
async with self._lock:
|
||||
self._known = {
|
||||
package.base: (package, status)
|
||||
for package, status in self.client.package_get(None)
|
||||
}
|
||||
|
||||
async def logs_rotate(self, keep_last_records: int) -> None:
|
||||
"""
|
||||
remove older logs from storage
|
||||
|
||||
Args:
|
||||
keep_last_records(int): number of last records to keep
|
||||
"""
|
||||
self.client.logs_rotate(keep_last_records)
|
||||
|
||||
async def package_archives(self, package_base: str) -> list[Package]:
|
||||
"""
|
||||
get known package archives
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
list[Package]: list of built package for this package base
|
||||
"""
|
||||
return self.package_info.package_archives(package_base)
|
||||
|
||||
async def package_changes_get(self, package_base: str) -> Changes:
|
||||
"""
|
||||
get package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
|
||||
Returns:
|
||||
Changes: package changes if available and empty object otherwise
|
||||
"""
|
||||
return self.client.package_changes_get(package_base)
|
||||
|
||||
async def package_changes_update(self, package_base: str, changes: Changes) -> None:
|
||||
"""
|
||||
update package changes
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
changes(Changes): changes descriptor
|
||||
"""
|
||||
self.client.package_changes_update(package_base, changes)
|
||||
|
||||
async def package_dependencies_get(self, package_base: str) -> Dependencies:
|
||||
"""
|
||||
get package dependencies
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
|
||||
Returns:
|
||||
list[Dependencies]: package implicit dependencies if available
|
||||
"""
|
||||
return self.client.package_dependencies_get(package_base)
|
||||
|
||||
async def package_dependencies_update(self, package_base: str, dependencies: Dependencies) -> None:
|
||||
"""
|
||||
update package dependencies
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
dependencies(Dependencies): dependencies descriptor
|
||||
"""
|
||||
self.client.package_dependencies_update(package_base, dependencies)
|
||||
|
||||
async def package_get(self, package_base: str) -> tuple[Package, BuildStatus]:
|
||||
"""
|
||||
get current package base build status
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
|
||||
Returns:
|
||||
tuple[Package, BuildStatus]: package and its status
|
||||
|
||||
Raises:
|
||||
UnknownPackageError: if no package found
|
||||
"""
|
||||
try:
|
||||
async with self._lock:
|
||||
return self._known[package_base]
|
||||
except KeyError:
|
||||
raise UnknownPackageError(package_base) from None
|
||||
|
||||
async def package_hold_update(self, package_base: str, *, enabled: bool) -> None:
|
||||
"""
|
||||
update package hold status
|
||||
|
||||
Args:
|
||||
package_base(str): package base name
|
||||
enabled(bool): new hold status
|
||||
"""
|
||||
package, status = await self.package_get(package_base)
|
||||
async with self._lock:
|
||||
self._known[package_base] = (package, replace(status, is_held=enabled))
|
||||
self.client.package_hold_update(package_base, enabled=enabled)
|
||||
|
||||
await self.event_bus.broadcast(EventType.PackageHeld, package_base, is_held=enabled)
|
||||
|
||||
async def package_logs_add(self, log_record: LogRecord) -> None:
|
||||
"""
|
||||
post log record
|
||||
|
||||
Args:
|
||||
log_record(LogRecord): log record
|
||||
"""
|
||||
self.client.package_logs_add(log_record)
|
||||
|
||||
await self.event_bus.broadcast(EventType.BuildLog, log_record.log_record_id.package_base, **log_record.view())
|
||||
|
||||
async def package_logs_get(self, package_base: str, version: str | None = None, process_id: str | None = None,
|
||||
limit: int = -1, offset: int = 0) -> list[LogRecord]:
|
||||
"""
|
||||
get package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None, optional): package version to search (Default value = None)
|
||||
process_id(str | None, optional): process identifier to search (Default value = None)
|
||||
limit(int, optional): limit records to the specified count, -1 means unlimited (Default value = -1)
|
||||
offset(int, optional): records offset (Default value = 0)
|
||||
|
||||
Returns:
|
||||
list[LogRecord]: package logs
|
||||
"""
|
||||
return self.client.package_logs_get(package_base, version, process_id, limit, offset)
|
||||
|
||||
async def package_logs_remove(self, package_base: str, version: str | None) -> None:
|
||||
"""
|
||||
remove package logs
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
version(str | None): package version to remove logs. If ``None`` is set, all logs will be removed
|
||||
"""
|
||||
self.client.package_logs_remove(package_base, version)
|
||||
|
||||
async def package_patches_get(self, package_base: str, variable: str | None) -> list[PkgbuildPatch]:
|
||||
"""
|
||||
get package patches
|
||||
|
||||
Args:
|
||||
package_base(str): package base to retrieve
|
||||
variable(str | None): optional filter by patch variable
|
||||
|
||||
Returns:
|
||||
list[PkgbuildPatch]: list of patches for the specified package
|
||||
"""
|
||||
return self.client.package_patches_get(package_base, variable)
|
||||
|
||||
async def package_patches_remove(self, package_base: str, variable: str | None) -> None:
|
||||
"""
|
||||
remove package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
variable(str | None): patch name. If ``None`` is set, all patches will be removed
|
||||
"""
|
||||
self.client.package_patches_remove(package_base, variable)
|
||||
|
||||
async def package_patches_update(self, package_base: str, patch: PkgbuildPatch) -> None:
|
||||
"""
|
||||
create or update package patch
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
patch(PkgbuildPatch): package patch
|
||||
"""
|
||||
self.client.package_patches_update(package_base, patch)
|
||||
|
||||
async def package_remove(self, package_base: str) -> None:
|
||||
"""
|
||||
remove package base from known list if any
|
||||
|
||||
Args:
|
||||
package_base(str): package base
|
||||
"""
|
||||
async with self._lock:
|
||||
self._known.pop(package_base, None)
|
||||
self.client.package_remove(package_base)
|
||||
|
||||
await self.event_bus.broadcast(EventType.PackageRemoved, package_base)
|
||||
|
||||
async def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package status
|
||||
|
||||
Args:
|
||||
package_base(str): package base to update
|
||||
status(BuildStatusEnum): new build status
|
||||
"""
|
||||
package, current_status = await self.package_get(package_base)
|
||||
async with self._lock:
|
||||
self._known[package_base] = (package, BuildStatus(status, is_held=current_status.is_held))
|
||||
self.client.package_status_update(package_base, status)
|
||||
|
||||
await self.event_bus.broadcast(EventType.PackageStatusChanged, package_base, status=status.value)
|
||||
|
||||
async def package_update(self, package: Package, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update package
|
||||
|
||||
Args:
|
||||
package(Package): package description
|
||||
status(BuildStatusEnum): new build status
|
||||
"""
|
||||
async with self._lock:
|
||||
_, current_status = self._known.get(package.base, (package, BuildStatus()))
|
||||
self._known[package.base] = (package, BuildStatus(status, is_held=current_status.is_held))
|
||||
self.client.package_update(package, status)
|
||||
|
||||
await self.event_bus.broadcast(
|
||||
EventType.PackageUpdated, package.base, status=status.value, version=package.version,
|
||||
)
|
||||
|
||||
async def packages(self) -> list[tuple[Package, BuildStatus]]:
|
||||
"""
|
||||
get current known packages list
|
||||
|
||||
Returns:
|
||||
list[tuple[Package, BuildStatus]]: list of packages together with their statuses
|
||||
"""
|
||||
async with self._lock:
|
||||
return list(self._known.values())
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
gracefully shutdown watcher
|
||||
"""
|
||||
await self.event_bus.shutdown()
|
||||
|
||||
async def status_update(self, status: BuildStatusEnum) -> None:
|
||||
"""
|
||||
update service status
|
||||
|
||||
Args:
|
||||
status(BuildStatusEnum): new service status
|
||||
"""
|
||||
self.status = BuildStatus(status)
|
||||
|
||||
await self.event_bus.broadcast(EventType.ServiceStatusChanged, None, status=status.value)
|
||||
|
||||
def __call__(self, package_base: str | None) -> Self:
|
||||
"""
|
||||
extract client for future calls
|
||||
|
||||
Args:
|
||||
package_base(str | None): package base to validate that package exists if applicable
|
||||
|
||||
Returns:
|
||||
Self: instance of self to pass calls to the client
|
||||
|
||||
Raises:
|
||||
UnknownPackageError: if no package found
|
||||
"""
|
||||
# keep check here instead of calling package_get to keep this method synchronized
|
||||
if package_base is not None and package_base not in self._known:
|
||||
raise UnknownPackageError(package_base)
|
||||
return self
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
@@ -1,76 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 dataclasses import dataclass
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogRecord:
|
||||
"""
|
||||
log record
|
||||
|
||||
Attributes:
|
||||
log_record_id(LogRecordId): log record identifier
|
||||
created(float): log record creation timestamp
|
||||
message(str): log record message
|
||||
"""
|
||||
|
||||
log_record_id: LogRecordId
|
||||
created: float
|
||||
message: str
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, package_base: str, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct log record from the JSON dump
|
||||
|
||||
Args:
|
||||
package_base(str): package base for which log record belongs
|
||||
dump(dict[str, Any]): JSON dump body
|
||||
|
||||
Returns:
|
||||
Self: log record object
|
||||
"""
|
||||
if "process_id" in dump:
|
||||
log_record_id = LogRecordId(package_base, dump["version"], dump["process_id"])
|
||||
else:
|
||||
log_record_id = LogRecordId(package_base, dump["version"])
|
||||
|
||||
return cls(
|
||||
log_record_id=log_record_id,
|
||||
created=dump["created"],
|
||||
message=dump["message"],
|
||||
)
|
||||
|
||||
def view(self) -> dict[str, Any]:
|
||||
"""
|
||||
generate JSON log record view
|
||||
|
||||
Returns:
|
||||
dict[str, Any]: json-friendly dictionary
|
||||
"""
|
||||
return {
|
||||
"created": self.created,
|
||||
"message": self.message,
|
||||
"version": self.log_record_id.version,
|
||||
"process_id": self.log_record_id.process_id,
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import uuid
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogRecordId:
|
||||
"""
|
||||
log record process identifier
|
||||
|
||||
Attributes:
|
||||
DEFAULT_PROCESS_ID(str): (class attribute) default process identifier
|
||||
package_base(str): package base for which log record belongs
|
||||
version(str): package version for which log record belongs
|
||||
process_id(str, optional): unique process identifier
|
||||
"""
|
||||
|
||||
package_base: str
|
||||
version: str
|
||||
process_id: str = ""
|
||||
|
||||
DEFAULT_PROCESS_ID: ClassVar[str] = str(uuid.uuid4())
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""
|
||||
assign process identifier from default if not set
|
||||
"""
|
||||
if not self.process_id:
|
||||
object.__setattr__(self, "process_id", self.DEFAULT_PROCESS_ID)
|
||||
@@ -1,77 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 dataclasses import dataclass, fields
|
||||
from typing import Any, Self
|
||||
|
||||
from ahriman.core.utils import filter_json
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RepositoryStats:
|
||||
"""
|
||||
repository stats representation
|
||||
"""
|
||||
|
||||
bases: int
|
||||
packages: int
|
||||
archive_size: int
|
||||
installed_size: int
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, dump: dict[str, Any]) -> Self:
|
||||
"""
|
||||
construct counters from JSON dump
|
||||
|
||||
Args:
|
||||
dump(dict[str, Any]): JSON dump body
|
||||
|
||||
Returns:
|
||||
Self: status counters
|
||||
"""
|
||||
# filter to only known fields
|
||||
known_fields = [pair.name for pair in fields(cls)]
|
||||
return cls(**filter_json(dump, known_fields))
|
||||
|
||||
@classmethod
|
||||
def from_packages(cls, packages: list[Package]) -> Self:
|
||||
"""
|
||||
construct statistics from list of repository packages
|
||||
|
||||
Args:
|
||||
packages(list[Packages]): list of repository packages
|
||||
|
||||
Returns:
|
||||
Self: constructed statistics object
|
||||
"""
|
||||
return cls(
|
||||
bases=len(packages),
|
||||
packages=sum(len(package.packages) for package in packages),
|
||||
archive_size=sum(
|
||||
archive.archive_size or 0
|
||||
for package in packages
|
||||
for archive in package.packages.values()
|
||||
),
|
||||
installed_size=sum(
|
||||
archive.installed_size or 0
|
||||
for package in packages
|
||||
for archive in package.packages.values()
|
||||
),
|
||||
)
|
||||
@@ -1,104 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import statistics
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SeriesStatistics:
|
||||
"""
|
||||
series statistics helper
|
||||
|
||||
Attributes:
|
||||
series(list[float | int]): list of values to be processed
|
||||
"""
|
||||
|
||||
series: list[float | int]
|
||||
|
||||
@property
|
||||
def max(self) -> float | int | None:
|
||||
"""
|
||||
get max value in series
|
||||
|
||||
Returns:
|
||||
float | int | None: ``None`` if series is empty and maximal value otherwise``
|
||||
"""
|
||||
if self:
|
||||
return max(self.series)
|
||||
return None
|
||||
|
||||
@property
|
||||
def mean(self) -> float | int | None:
|
||||
"""
|
||||
get mean value in series
|
||||
|
||||
Returns:
|
||||
float | int | None: ``None`` if series is empty and mean value otherwise
|
||||
"""
|
||||
if self:
|
||||
return statistics.mean(self.series)
|
||||
return None
|
||||
|
||||
@property
|
||||
def min(self) -> float | int | None:
|
||||
"""
|
||||
get min value in series
|
||||
|
||||
Returns:
|
||||
float | int | None: ``None`` if series is empty and minimal value otherwise
|
||||
"""
|
||||
if self:
|
||||
return min(self.series)
|
||||
return None
|
||||
|
||||
@property
|
||||
def st_dev(self) -> float | None:
|
||||
"""
|
||||
get standard deviation in series
|
||||
|
||||
Returns:
|
||||
float | None: ``None`` if series size is less than 1, 0 if series contains single element and standard
|
||||
deviation otherwise
|
||||
"""
|
||||
if not self:
|
||||
return None
|
||||
if len(self.series) > 1:
|
||||
return statistics.stdev(self.series)
|
||||
return 0.0
|
||||
|
||||
@property
|
||||
def total(self) -> int:
|
||||
"""
|
||||
retrieve amount of elements
|
||||
|
||||
Returns:
|
||||
int: the series collection size
|
||||
"""
|
||||
return len(self.series)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""
|
||||
check if series is empty or not
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if series contains elements and ``False`` otherwise
|
||||
"""
|
||||
return bool(self.total)
|
||||
@@ -1,85 +0,0 @@
|
||||
import argparse
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.add import Add
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line arguments fixture
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: generated arguments for these test cases
|
||||
"""
|
||||
args.package = ["ahriman"]
|
||||
args.now = False
|
||||
args.refresh = 0
|
||||
args.source = PackageSource.Auto
|
||||
args.username = "username"
|
||||
args.variable = None
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
|
||||
perform_mock = mocker.patch("ahriman.application.handlers.add.Add.perform_action")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Add.run(args, repository_id, configuration, report=False)
|
||||
on_start_mock.assert_called_once_with()
|
||||
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
|
||||
|
||||
|
||||
def test_perform_action(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must perform add action
|
||||
"""
|
||||
args = _default_args(args)
|
||||
application_mock = mocker.patch("ahriman.application.application.Application.add")
|
||||
update_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
|
||||
|
||||
Add.perform_action(application, args)
|
||||
application_mock.assert_called_once_with(args.package, args.source, args.username)
|
||||
update_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_perform_action_with_patches(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must perform add action and insert temporary patches
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.variable = ["KEY=VALUE"]
|
||||
mocker.patch("ahriman.application.application.Application.add")
|
||||
patches_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_update")
|
||||
|
||||
Add.perform_action(application, args)
|
||||
patches_mock.assert_called_once_with(args.package[0], PkgbuildPatch("KEY", "VALUE"))
|
||||
|
||||
|
||||
def test_perform_action_with_updates(args: argparse.Namespace, application: Application, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must perform add action with updates after
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.now = True
|
||||
mocker.patch("ahriman.application.application.Application.add")
|
||||
update_mock = mocker.patch("ahriman.application.handlers.update.Update.perform_action")
|
||||
|
||||
Add.perform_action(application, args)
|
||||
update_mock.assert_called_once_with(application, args)
|
||||
@@ -1,84 +0,0 @@
|
||||
import argparse
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers.archives import Archives
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line arguments fixture
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: generated arguments for these test cases
|
||||
"""
|
||||
args.action = Action.List
|
||||
args.exit_code = False
|
||||
args.info = False
|
||||
args.package = "package"
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
application_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
|
||||
return_value=[package_ahriman])
|
||||
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Archives.run(args, repository_id, configuration, report=False)
|
||||
application_mock.assert_called_once_with(args.package)
|
||||
check_mock.assert_called_once_with(False, True)
|
||||
print_mock.assert_called_once_with(verbose=False, log_fn=pytest.helpers.anyvar(int), separator=": ")
|
||||
|
||||
|
||||
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise ExitCode exception on empty archives result
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.exit_code = True
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
|
||||
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Archives.run(args, repository_id, configuration, report=False)
|
||||
check_mock.assert_called_once_with(True, False)
|
||||
|
||||
|
||||
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create application object with native reporting
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||
load_mock = mocker.patch("ahriman.core.repository.Repository.load")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Archives.run(args, repository_id, configuration, report=False)
|
||||
load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
|
||||
|
||||
|
||||
def test_disallow_multi_architecture_run() -> None:
|
||||
"""
|
||||
must not allow multi architecture run
|
||||
"""
|
||||
assert not Archives.ALLOW_MULTI_ARCHITECTURE_RUN
|
||||
@@ -1,53 +0,0 @@
|
||||
import argparse
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers.hold import Hold
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.action import Action
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line arguments fixture
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: generated arguments for these test cases
|
||||
"""
|
||||
args.package = ["ahriman"]
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.action = Action.Update
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Hold.run(args, repository_id, configuration, report=False)
|
||||
hold_mock.assert_called_once_with("ahriman", enabled=True)
|
||||
|
||||
|
||||
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove held status
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.action = Action.Remove
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Hold.run(args, repository_id, configuration, report=False)
|
||||
hold_mock.assert_called_once_with("ahriman", enabled=False)
|
||||
@@ -1,100 +0,0 @@
|
||||
import argparse
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers.pkgbuild import Pkgbuild
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.action import Action
|
||||
from ahriman.models.changes import Changes
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line arguments fixture
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: generated arguments for these test cases
|
||||
"""
|
||||
args.action = Action.List
|
||||
args.exit_code = False
|
||||
args.package = "package"
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
application_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
|
||||
return_value=Changes("sha", "change", "pkgbuild content"))
|
||||
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||
print_mock = mocker.patch("ahriman.core.formatters.Printer.print")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||
application_mock.assert_called_once_with(args.package)
|
||||
check_mock.assert_called_once_with(False, True)
|
||||
print_mock.assert_called_once_with(verbose=True, log_fn=pytest.helpers.anyvar(int), separator="")
|
||||
|
||||
|
||||
def test_run_empty_exception(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise ExitCode exception on empty pkgbuild result
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.exit_code = True
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=Changes())
|
||||
check_mock = mocker.patch("ahriman.application.handlers.handler.Handler.check_status")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||
check_mock.assert_called_once_with(True, False)
|
||||
|
||||
|
||||
def test_run_remove(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove package pkgbuild
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.action = Action.Remove
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
changes = Changes("sha", "change", "pkgbuild content")
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get", return_value=changes)
|
||||
update_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||
update_mock.assert_called_once_with(args.package, Changes("sha", "change", None))
|
||||
|
||||
|
||||
def test_imply_with_report(args: argparse.Namespace, configuration: Configuration, database: SQLite,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create application object with native reporting
|
||||
"""
|
||||
args = _default_args(args)
|
||||
mocker.patch("ahriman.core.database.SQLite.load", return_value=database)
|
||||
load_mock = mocker.patch("ahriman.core.repository.Repository.load")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Pkgbuild.run(args, repository_id, configuration, report=False)
|
||||
load_mock.assert_called_once_with(repository_id, configuration, database, report=True, refresh_pacman_database=0)
|
||||
|
||||
|
||||
def test_disallow_multi_architecture_run() -> None:
|
||||
"""
|
||||
must not allow multi architecture run
|
||||
"""
|
||||
assert not Pkgbuild.ALLOW_MULTI_ARCHITECTURE_RUN
|
||||
@@ -1,27 +0,0 @@
|
||||
import argparse
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.handlers.reload import Reload
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Repository
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
run_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.configuration_reload")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Reload.run(args, repository_id, configuration, report=False)
|
||||
run_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_disallow_multi_architecture_run() -> None:
|
||||
"""
|
||||
must not allow multi architecture run
|
||||
"""
|
||||
assert not Reload.ALLOW_MULTI_ARCHITECTURE_RUN
|
||||
@@ -1,113 +0,0 @@
|
||||
import argparse
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.application.application import Application
|
||||
from ahriman.application.handlers.rollback import Rollback
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def _default_args(args: argparse.Namespace) -> argparse.Namespace:
|
||||
"""
|
||||
default arguments for these test cases
|
||||
|
||||
Args:
|
||||
args(argparse.Namespace): command line arguments fixture
|
||||
|
||||
Returns:
|
||||
argparse.Namespace: generated arguments for these test cases
|
||||
"""
|
||||
args.package = "ahriman"
|
||||
args.version = "1.0.0-1"
|
||||
args.hold = False
|
||||
return args
|
||||
|
||||
|
||||
def test_run(args: argparse.Namespace, configuration: Configuration, repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must run command
|
||||
"""
|
||||
args = _default_args(args)
|
||||
artifacts = [package.filepath for package in package_ahriman.packages.values()]
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
on_start_mock = mocker.patch("ahriman.application.application.Application.on_start")
|
||||
load_mock = mocker.patch("ahriman.application.handlers.rollback.Rollback.package_load",
|
||||
return_value=package_ahriman)
|
||||
artifacts_mock = mocker.patch("ahriman.application.handlers.rollback.Rollback.package_artifacts",
|
||||
return_value=artifacts)
|
||||
perform_mock = mocker.patch("ahriman.application.handlers.add.Add.perform_action")
|
||||
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Rollback.run(args, repository_id, configuration, report=False)
|
||||
on_start_mock.assert_called_once_with()
|
||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman.base, args.version)
|
||||
artifacts_mock.assert_called_once_with(pytest.helpers.anyvar(int), package_ahriman)
|
||||
perform_mock.assert_called_once_with(pytest.helpers.anyvar(int), args)
|
||||
hold_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_run_hold(args: argparse.Namespace, configuration: Configuration, repository: Repository,
|
||||
package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must hold package after rollback
|
||||
"""
|
||||
args = _default_args(args)
|
||||
args.hold = True
|
||||
mocker.patch("ahriman.core.repository.Repository.load", return_value=repository)
|
||||
mocker.patch("ahriman.application.application.Application.on_start")
|
||||
mocker.patch("ahriman.application.handlers.rollback.Rollback.package_load", return_value=package_ahriman)
|
||||
mocker.patch("ahriman.application.handlers.rollback.Rollback.package_artifacts", return_value=[])
|
||||
mocker.patch("ahriman.application.handlers.add.Add.perform_action")
|
||||
hold_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||
|
||||
_, repository_id = configuration.check_loaded()
|
||||
Rollback.run(args, repository_id, configuration, report=False)
|
||||
hold_mock.assert_called_once_with(package_ahriman.base, enabled=True)
|
||||
|
||||
|
||||
def test_package_artifacts(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return package artifacts
|
||||
"""
|
||||
artifacts = [package.filepath for package in package_ahriman.packages.values()]
|
||||
lookup_mock = mocker.patch("ahriman.core.repository.Repository.package_archives_lookup", return_value=artifacts)
|
||||
|
||||
assert Rollback.package_artifacts(application, package_ahriman) == artifacts
|
||||
lookup_mock.assert_called_once_with(package_ahriman)
|
||||
|
||||
|
||||
def test_package_artifacts_empty(application: Application, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise UnknownPackageError if no artifacts found
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.Repository.package_archives_lookup", return_value=[])
|
||||
with pytest.raises(UnknownPackageError):
|
||||
Rollback.package_artifacts(application, package_ahriman)
|
||||
|
||||
|
||||
def test_package_load(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load package from reporter
|
||||
"""
|
||||
package_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
|
||||
return_value=[(package_ahriman, None)])
|
||||
|
||||
result = Rollback.package_load(application, package_ahriman.base, "2.0.0-1")
|
||||
assert result.version == "2.0.0-1"
|
||||
package_mock.assert_called_once_with(package_ahriman.base)
|
||||
|
||||
|
||||
def test_package_load_unknown(application: Application, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise UnknownPackageError if package not found
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[])
|
||||
with pytest.raises(UnknownPackageError):
|
||||
Rollback.package_load(application, package_ahriman.base, package_ahriman.version)
|
||||
@@ -1,148 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from dataclasses import replace
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import Remote
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
|
||||
|
||||
def test_info(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call info method
|
||||
"""
|
||||
info_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_info", return_value=aur_package_ahriman)
|
||||
assert Remote.info(aur_package_ahriman.name, pacman=pacman) == aur_package_ahriman
|
||||
info_mock.assert_called_once_with(aur_package_ahriman.name, pacman=pacman)
|
||||
|
||||
|
||||
def test_info_not_found(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise UnknownPackageError if no package found and search by provides is disabled
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.remote.Remote.package_info",
|
||||
side_effect=UnknownPackageError(aur_package_ahriman.name))
|
||||
with pytest.raises(UnknownPackageError):
|
||||
Remote.info(aur_package_ahriman.name, pacman=pacman)
|
||||
|
||||
|
||||
def test_info_include_provides(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must perform search through provides list is set
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.remote.Remote.package_info",
|
||||
side_effect=UnknownPackageError(aur_package_ahriman.name))
|
||||
provided_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_provided_by",
|
||||
return_value=[aur_package_ahriman])
|
||||
|
||||
assert Remote.info(aur_package_ahriman.name, pacman=pacman, include_provides=True) == aur_package_ahriman
|
||||
provided_mock.assert_called_once_with(aur_package_ahriman.name, pacman=pacman)
|
||||
|
||||
|
||||
def test_info_include_provides_not_found(aur_package_ahriman: AURPackage, pacman: Pacman,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must raise UnknownPackageError if no package found and search by provides returns empty list
|
||||
"""
|
||||
mocker.patch("ahriman.core.alpm.remote.Remote.package_info",
|
||||
side_effect=UnknownPackageError(aur_package_ahriman.name))
|
||||
mocker.patch("ahriman.core.alpm.remote.Remote.package_provided_by", return_value=[])
|
||||
|
||||
with pytest.raises(UnknownPackageError):
|
||||
Remote.info("ahriman", pacman=pacman, include_provides=True)
|
||||
|
||||
|
||||
def test_multisearch(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must search in AUR with multiple words
|
||||
"""
|
||||
terms = ["ahriman", "is", "cool"]
|
||||
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[aur_package_ahriman])
|
||||
|
||||
assert Remote.multisearch(*terms, pacman=pacman, search_by="name") == [aur_package_ahriman]
|
||||
search_mock.assert_has_calls([
|
||||
MockCall("ahriman", pacman=pacman, search_by="name"),
|
||||
MockCall("cool", pacman=pacman, search_by="name"),
|
||||
])
|
||||
|
||||
|
||||
def test_multisearch_empty(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return empty list if no long terms supplied
|
||||
"""
|
||||
terms = ["it", "is"]
|
||||
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
|
||||
|
||||
assert Remote.multisearch(*terms, pacman=pacman) == []
|
||||
search_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_multisearch_single(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must search in AUR with one word
|
||||
"""
|
||||
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[aur_package_ahriman])
|
||||
assert Remote.multisearch("ahriman", pacman=pacman) == [aur_package_ahriman]
|
||||
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by=None)
|
||||
|
||||
|
||||
def test_multisearch_remove_duplicates(aur_package_ahriman: AURPackage, pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove duplicates from search result
|
||||
"""
|
||||
package1 = replace(aur_package_ahriman)
|
||||
package2 = replace(aur_package_ahriman, name="ahriman-triggers")
|
||||
mocker.patch("ahriman.core.alpm.remote.Remote.package_search", return_value=[package1, package2])
|
||||
|
||||
assert Remote.multisearch("ahriman", pacman=pacman) == [package1]
|
||||
|
||||
|
||||
def test_remote_git_url(remote: Remote) -> None:
|
||||
"""
|
||||
must raise NotImplemented for missing remote git url
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
remote.remote_git_url("package", "repositories")
|
||||
|
||||
|
||||
def test_remote_web_url(remote: Remote) -> None:
|
||||
"""
|
||||
must raise NotImplemented for missing remote web url
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
remote.remote_web_url("package")
|
||||
|
||||
|
||||
def test_search(pacman: Pacman, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must call search method
|
||||
"""
|
||||
search_mock = mocker.patch("ahriman.core.alpm.remote.Remote.package_search")
|
||||
Remote.search("ahriman", pacman=pacman, search_by="name")
|
||||
search_mock.assert_called_once_with("ahriman", pacman=pacman, search_by="name")
|
||||
|
||||
|
||||
def test_package_info(remote: Remote, pacman: Pacman) -> None:
|
||||
"""
|
||||
must raise NotImplemented for missing package info method
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
remote.package_info("package", pacman=pacman)
|
||||
|
||||
|
||||
def test_package_provided_by(remote: Remote, pacman: Pacman) -> None:
|
||||
"""
|
||||
must return empty list for provides method
|
||||
"""
|
||||
assert remote.package_provided_by("package", pacman=pacman) == []
|
||||
|
||||
|
||||
def test_package_search(remote: Remote, pacman: Pacman) -> None:
|
||||
"""
|
||||
must raise NotImplemented for missing package search method
|
||||
"""
|
||||
with pytest.raises(NotImplementedError):
|
||||
remote.package_search("package", pacman=pacman, search_by=None)
|
||||
@@ -1,37 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.alpm.pacman_handle import PacmanHandle
|
||||
|
||||
|
||||
def test_package_load() -> None:
|
||||
"""
|
||||
must load package from archive path
|
||||
"""
|
||||
local = Path("local")
|
||||
instance = PacmanHandle.ephemeral()
|
||||
handle_mock = instance.handle = MagicMock()
|
||||
|
||||
instance.package_load(local)
|
||||
handle_mock.load_pkg.assert_called_once_with(str(local))
|
||||
|
||||
PacmanHandle._ephemeral = None
|
||||
|
||||
|
||||
def test_getattr() -> None:
|
||||
"""
|
||||
must proxy attribute access to underlying handle
|
||||
"""
|
||||
instance = PacmanHandle.ephemeral()
|
||||
assert instance.dbpath
|
||||
|
||||
|
||||
def test_getattr_not_found() -> None:
|
||||
"""
|
||||
must raise AttributeError for missing handle attributes
|
||||
"""
|
||||
instance = PacmanHandle.ephemeral()
|
||||
with pytest.raises(AttributeError):
|
||||
assert instance.random_attribute
|
||||
@@ -1,111 +0,0 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.build_tools.package_version import PackageVersion
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild import Pkgbuild
|
||||
|
||||
|
||||
def test_actual_version(package_ahriman: Package, configuration: Configuration) -> None:
|
||||
"""
|
||||
must return same actual_version as version is
|
||||
"""
|
||||
assert PackageVersion(package_ahriman).actual_version(configuration) == package_ahriman.version
|
||||
|
||||
|
||||
def test_actual_version_vcs(package_tpacpi_bat_git: Package, configuration: Configuration,
|
||||
mocker: MockerFixture, resource_path_root: Path) -> None:
|
||||
"""
|
||||
must return valid actual_version for VCS package
|
||||
"""
|
||||
pkgbuild = resource_path_root / "models" / "package_tpacpi-bat-git_pkgbuild"
|
||||
mocker.patch("ahriman.models.pkgbuild.Pkgbuild.from_file", return_value=Pkgbuild.from_file(pkgbuild))
|
||||
mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
|
||||
init_mock = mocker.patch("ahriman.core.build_tools.task.Task.init")
|
||||
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
||||
|
||||
assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == "3.1.r13.g4959b52-1"
|
||||
init_mock.assert_called_once_with(configuration.repository_paths.cache_for(package_tpacpi_bat_git.base), [], None)
|
||||
unlink_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_actual_version_failed(package_tpacpi_bat_git: Package, configuration: Configuration,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return same version in case if exception occurred
|
||||
"""
|
||||
mocker.patch("ahriman.core.build_tools.task.Task.init", side_effect=Exception)
|
||||
mocker.patch("pathlib.Path.glob", return_value=[Path("local")])
|
||||
unlink_mock = mocker.patch("pathlib.Path.unlink")
|
||||
|
||||
assert PackageVersion(package_tpacpi_bat_git).actual_version(configuration) == package_tpacpi_bat_git.version
|
||||
unlink_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_is_newer_than(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must correctly check if package is newer than specified timestamp
|
||||
"""
|
||||
# base checks, true/false
|
||||
older = package_ahriman.packages[package_ahriman.base].build_date - 1
|
||||
assert PackageVersion(package_ahriman).is_newer_than(older)
|
||||
|
||||
newer = package_ahriman.packages[package_ahriman.base].build_date + 1
|
||||
assert not PackageVersion(package_ahriman).is_newer_than(newer)
|
||||
|
||||
# list check
|
||||
min_date = min(package.build_date for package in package_python_schedule.packages.values())
|
||||
assert PackageVersion(package_python_schedule).is_newer_than(min_date)
|
||||
|
||||
# null list check
|
||||
package_python_schedule.packages["python-schedule"].build_date = None
|
||||
assert PackageVersion(package_python_schedule).is_newer_than(min_date)
|
||||
|
||||
package_python_schedule.packages["python2-schedule"].build_date = None
|
||||
assert not PackageVersion(package_python_schedule).is_newer_than(min_date)
|
||||
|
||||
|
||||
def test_is_outdated_false(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must be not outdated for the same package
|
||||
"""
|
||||
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version",
|
||||
return_value=package_ahriman.version)
|
||||
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration)
|
||||
actual_version_mock.assert_called_once_with(configuration)
|
||||
|
||||
|
||||
def test_is_outdated_true(package_ahriman: Package, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must be outdated for the new version
|
||||
"""
|
||||
other = Package.from_json(package_ahriman.view())
|
||||
other.version = other.version.replace("-1", "-2")
|
||||
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version",
|
||||
return_value=other.version)
|
||||
|
||||
assert PackageVersion(package_ahriman).is_outdated(other, configuration)
|
||||
actual_version_mock.assert_called_once_with(configuration)
|
||||
|
||||
|
||||
def test_is_outdated_no_version_calculation(package_ahriman: Package, configuration: Configuration,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must not call actual version if calculation is disabled
|
||||
"""
|
||||
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version")
|
||||
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration, calculate_version=False)
|
||||
actual_version_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_is_outdated_fresh_package(package_ahriman: Package, configuration: Configuration,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must not call actual version if package is never than specified time
|
||||
"""
|
||||
configuration.set_option("build", "vcs_allowed_age", str(int(utcnow().timestamp())))
|
||||
actual_version_mock = mocker.patch("ahriman.core.build_tools.package_version.PackageVersion.actual_version")
|
||||
assert not PackageVersion(package_ahriman).is_outdated(package_ahriman, configuration)
|
||||
actual_version_mock.assert_not_called()
|
||||
@@ -1,8 +0,0 @@
|
||||
from ahriman.core.database.migrations.m015_logs_process_id import steps
|
||||
|
||||
|
||||
def test_migration_logs_process_id() -> None:
|
||||
"""
|
||||
migration must not be empty
|
||||
"""
|
||||
assert steps
|
||||
@@ -1,77 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from sqlite3 import Connection
|
||||
from typing import Any
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.database.migrations.m016_archive import migrate_data, move_packages
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
def test_migrate_data(connection: Connection, configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must perform data migration
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
repositories = [
|
||||
repository_id,
|
||||
replace(repository_id, architecture="i686"),
|
||||
]
|
||||
mocker.patch("ahriman.core.repository.Explorer.repositories_extract", return_value=repositories)
|
||||
migration_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.move_packages")
|
||||
|
||||
migrate_data(connection, configuration)
|
||||
migration_mock.assert_has_calls([
|
||||
MockCall(replace(configuration.repository_paths, repository_id=repository))
|
||||
for repository in repositories
|
||||
])
|
||||
|
||||
|
||||
def test_move_packages(repository_paths: RepositoryPaths, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must move packages to the archive directory
|
||||
"""
|
||||
|
||||
def is_file(self: Path, *args: Any, **kwargs: Any) -> bool:
|
||||
return "file" in self.name
|
||||
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[
|
||||
repository_paths.repository / ".hidden-file.pkg.tar.xz",
|
||||
repository_paths.repository / "directory",
|
||||
repository_paths.repository / "file.pkg.tar.xz",
|
||||
repository_paths.repository / "file.pkg.tar.xz.sig",
|
||||
repository_paths.repository / "file2.pkg.tar.xz",
|
||||
repository_paths.repository / "symlink.pkg.tar.xz",
|
||||
])
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
mocker.patch("pathlib.Path.is_file", autospec=True, side_effect=is_file)
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
archive_mock = mocker.patch("ahriman.models.package.Package.from_archive", return_value=package_ahriman)
|
||||
move_mock = mocker.patch("ahriman.core.database.migrations.m016_archive.atomic_move")
|
||||
symlink_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||
|
||||
move_packages(repository_paths)
|
||||
archive_mock.assert_has_calls([
|
||||
MockCall(repository_paths.repository / filename)
|
||||
for filename in ("file.pkg.tar.xz", "file2.pkg.tar.xz")
|
||||
])
|
||||
move_mock.assert_has_calls([
|
||||
MockCall(repository_paths.repository / filename, repository_paths.archive_for(package_ahriman.base) / filename)
|
||||
for filename in ("file.pkg.tar.xz", "file.pkg.tar.xz.sig", "file2.pkg.tar.xz")
|
||||
])
|
||||
symlink_mock.assert_has_calls([
|
||||
MockCall(
|
||||
Path("..") /
|
||||
".." /
|
||||
".." /
|
||||
repository_paths.archive_for(package_ahriman.base).relative_to(repository_paths.root) /
|
||||
filename
|
||||
)
|
||||
for filename in ("file.pkg.tar.xz", "file.pkg.tar.xz.sig", "file2.pkg.tar.xz")
|
||||
])
|
||||
@@ -1,8 +0,0 @@
|
||||
from ahriman.core.database.migrations.m017_pkgbuild import steps
|
||||
|
||||
|
||||
def test_migration_pkgbuild() -> None:
|
||||
"""
|
||||
migration must not be empty
|
||||
"""
|
||||
assert steps
|
||||
@@ -1,8 +0,0 @@
|
||||
from ahriman.core.database.migrations.m018_package_hold import steps
|
||||
|
||||
|
||||
def test_migration_package_hold() -> None:
|
||||
"""
|
||||
migration must not be empty
|
||||
"""
|
||||
assert steps
|
||||
@@ -1,169 +0,0 @@
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
def test_logs_insert_remove_version(database: SQLite, package_ahriman: Package,
|
||||
package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must clear version specific package logs
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3"))
|
||||
|
||||
database.logs_remove(package_ahriman.base, "1")
|
||||
assert database.logs_get(package_ahriman.base) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
||||
]
|
||||
assert database.logs_get(package_python_schedule.base) == [
|
||||
LogRecord(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_insert_remove_multi(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must clear logs for specified repository
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"),
|
||||
RepositoryId("i686", database._repository_id.name))
|
||||
|
||||
database.logs_remove(package_ahriman.base, None, RepositoryId("i686", database._repository_id.name))
|
||||
assert not database.logs_get(package_ahriman.base, repository_id=RepositoryId("i686", database._repository_id.name))
|
||||
assert database.logs_get(package_ahriman.base) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_insert_remove_full(database: SQLite, package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must clear full package logs
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3"))
|
||||
|
||||
database.logs_remove(package_ahriman.base, None)
|
||||
assert not database.logs_get(package_ahriman.base)
|
||||
assert database.logs_get(package_python_schedule.base) == [
|
||||
LogRecord(LogRecordId(package_python_schedule.base, "1"), 42.0, "message 3"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_insert_get(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get package logs
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
assert database.logs_get(package_ahriman.base) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_insert_get_pagination(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get package logs with pagination
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"))
|
||||
assert database.logs_get(package_ahriman.base, None, None, 1, 1) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_insert_get_filter_by_version(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get package logs with pagination
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p2"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 44.0, "message 3"))
|
||||
|
||||
assert database.logs_get(package_ahriman.base, "1", None) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"),
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1", "p2"), 43.0, "message 2"),
|
||||
]
|
||||
assert database.logs_get(package_ahriman.base, "2", None) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 44.0, "message 3"),
|
||||
]
|
||||
assert database.logs_get(package_ahriman.base, None, "p1") == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"),
|
||||
LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 44.0, "message 3"),
|
||||
]
|
||||
assert database.logs_get(package_ahriman.base, "1", "p1") == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_insert_get_multi(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must insert and get package logs for multiple repositories
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"),
|
||||
RepositoryId("i686", database._repository_id.name))
|
||||
|
||||
assert database.logs_get(package_ahriman.base,
|
||||
repository_id=RepositoryId("i686", database._repository_id.name)) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"),
|
||||
]
|
||||
assert database.logs_get(package_ahriman.base) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_rotate_remove_older(database: SQLite, package_ahriman: Package,
|
||||
package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must correctly remove old records
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2", "p2"), 44.0, "message 3"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2", "p2"), 45.0, "message 4"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_python_schedule.base, "3", "p1"), 40.0, "message 5"))
|
||||
|
||||
database.logs_rotate(1)
|
||||
assert database.logs_get(package_ahriman.base) == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "2", "p2"), 44.0, "message 3"),
|
||||
LogRecord(LogRecordId(package_ahriman.base, "2", "p2"), 45.0, "message 4"),
|
||||
]
|
||||
assert database.logs_get(package_python_schedule.base) == [
|
||||
LogRecord(LogRecordId(package_python_schedule.base, "3", "p1"), 40.0, "message 5"),
|
||||
]
|
||||
|
||||
|
||||
def test_logs_rotate_remove_all(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must remove all records when rotating with keep_last_records is 0
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2"), 44.0, "message 3"))
|
||||
|
||||
database.logs_rotate(0)
|
||||
assert not database.logs_get(package_ahriman.base)
|
||||
|
||||
|
||||
def test_logs_rotate_remove_duplicates(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must remove duplicate records while preserving the most recent one for each package version
|
||||
"""
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p1"), 42.0, "message 1"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p2"), 43.0, "message 2"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "1", "p3"), 44.0, "message 3"))
|
||||
database.logs_insert(LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 45.0, "message 4"))
|
||||
|
||||
database.logs_rotate(2)
|
||||
|
||||
logs = database.logs_get(package_ahriman.base)
|
||||
assert len(logs) == 2
|
||||
assert logs == [
|
||||
LogRecord(LogRecordId(package_ahriman.base, "1", "p3"), 44.0, "message 3"),
|
||||
LogRecord(LogRecordId(package_ahriman.base, "2", "p1"), 45.0, "message 4"),
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
from ahriman.core.formatters import PkgbuildPrinter
|
||||
from ahriman.models.changes import Changes
|
||||
|
||||
|
||||
def test_properties(pkgbuild_printer: PkgbuildPrinter) -> None:
|
||||
"""
|
||||
must return non-empty properties list
|
||||
"""
|
||||
assert pkgbuild_printer.properties()
|
||||
|
||||
|
||||
def test_properties_empty() -> None:
|
||||
"""
|
||||
must return empty properties list if pkgbuild is empty
|
||||
"""
|
||||
assert not PkgbuildPrinter(Changes()).properties()
|
||||
assert not PkgbuildPrinter(Changes("sha", "changes")).properties()
|
||||
|
||||
|
||||
def test_title(pkgbuild_printer: PkgbuildPrinter) -> None:
|
||||
"""
|
||||
must return non-empty title
|
||||
"""
|
||||
assert pkgbuild_printer.title()
|
||||
|
||||
|
||||
def test_title_empty() -> None:
|
||||
"""
|
||||
must return empty title if change is empty
|
||||
"""
|
||||
assert not PkgbuildPrinter(Changes()).title()
|
||||
assert not PkgbuildPrinter(Changes("sha")).title()
|
||||
@@ -1,15 +0,0 @@
|
||||
from ahriman.core.formatters import RepositoryStatsPrinter
|
||||
|
||||
|
||||
def test_properties(repository_stats_printer: RepositoryStatsPrinter) -> None:
|
||||
"""
|
||||
must return non-empty properties list
|
||||
"""
|
||||
assert repository_stats_printer.properties()
|
||||
|
||||
|
||||
def test_title(repository_stats_printer: RepositoryStatsPrinter) -> None:
|
||||
"""
|
||||
must return non-empty title
|
||||
"""
|
||||
assert repository_stats_printer.title()
|
||||
@@ -1,34 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.housekeeping import ArchiveRotationTrigger, LogsRotationTrigger
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archive_rotation_trigger(configuration: Configuration) -> ArchiveRotationTrigger:
|
||||
"""
|
||||
archive rotation trigger fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
ArchiveRotationTrigger: archive rotation trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return ArchiveRotationTrigger(repository_id, configuration)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logs_rotation_trigger(configuration: Configuration) -> LogsRotationTrigger:
|
||||
"""
|
||||
logs rotation trigger fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
LogsRotationTrigger: logs rotation trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return LogsRotationTrigger(repository_id, configuration)
|
||||
@@ -1,67 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.housekeeping import ArchiveRotationTrigger
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
def test_configuration_sections(configuration: Configuration) -> None:
|
||||
"""
|
||||
must correctly parse target list
|
||||
"""
|
||||
assert ArchiveRotationTrigger.configuration_sections(configuration) == ["archive"]
|
||||
|
||||
|
||||
def test_archives_remove(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
|
||||
repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove older packages
|
||||
"""
|
||||
packages = []
|
||||
for i in range(5):
|
||||
generated = replace(package_ahriman, version=str(i))
|
||||
generated.packages = {
|
||||
key: replace(value, filename=str(i))
|
||||
for key, value in generated.packages.items()
|
||||
}
|
||||
packages.append(generated)
|
||||
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=packages)
|
||||
mocker.patch("pathlib.Path.glob", return_value=[Path(str(i)) for i in range(5)])
|
||||
unlink_mock = mocker.patch("pathlib.Path.unlink", autospec=True)
|
||||
|
||||
archive_rotation_trigger.archives_remove(package_ahriman, repository)
|
||||
unlink_mock.assert_has_calls([
|
||||
MockCall(Path("0")),
|
||||
MockCall(Path("1")),
|
||||
])
|
||||
|
||||
|
||||
def test_archives_remove_keep(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
|
||||
repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must keep all packages if set to
|
||||
"""
|
||||
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives")
|
||||
|
||||
archive_rotation_trigger.keep_built_packages = 0
|
||||
archive_rotation_trigger.archives_remove(package_ahriman, repository)
|
||||
archives_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_on_result(archive_rotation_trigger: ArchiveRotationTrigger, package_ahriman: Package,
|
||||
package_python_schedule: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must rotate archives
|
||||
"""
|
||||
mocker.patch("ahriman.core._Context.get")
|
||||
remove_mock = mocker.patch("ahriman.core.housekeeping.ArchiveRotationTrigger.archives_remove")
|
||||
archive_rotation_trigger.on_result(Result(added=[package_ahriman], failed=[package_python_schedule]), [])
|
||||
remove_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
|
||||
@@ -1,26 +0,0 @@
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.housekeeping import LogsRotationTrigger
|
||||
from ahriman.core.status import Client
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
def test_configuration_sections(configuration: Configuration) -> None:
|
||||
"""
|
||||
must correctly parse target list
|
||||
"""
|
||||
assert LogsRotationTrigger.configuration_sections(configuration) == ["logs-rotation"]
|
||||
|
||||
|
||||
def test_on_result(logs_rotation_trigger: LogsRotationTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must rotate logs
|
||||
"""
|
||||
client_mock = MagicMock()
|
||||
context_mock = mocker.patch("ahriman.core._Context.get", return_value=client_mock)
|
||||
|
||||
logs_rotation_trigger.on_result(Result(), [])
|
||||
context_mock.assert_called_once_with(Client)
|
||||
client_mock.logs_rotate.assert_called_once_with(logs_rotation_trigger.keep_last_records)
|
||||
@@ -1,80 +0,0 @@
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.http import SyncAhrimanClient
|
||||
from ahriman.models.user import User
|
||||
|
||||
|
||||
def test_adapters(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must return native adapters
|
||||
"""
|
||||
assert "http+unix://" not in ahriman_client.adapters()
|
||||
|
||||
|
||||
def test_adapters_unix_socket(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must register unix socket adapter
|
||||
"""
|
||||
ahriman_client.address = "http+unix://path"
|
||||
assert "http+unix://" in ahriman_client.adapters()
|
||||
|
||||
|
||||
def test_login_url(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must generate login url correctly
|
||||
"""
|
||||
assert ahriman_client._login_url().startswith(ahriman_client.address)
|
||||
assert ahriman_client._login_url().endswith("/api/v1/login")
|
||||
|
||||
|
||||
def test_headers(ahriman_client: SyncAhrimanClient) -> None:
|
||||
"""
|
||||
must inject request id header
|
||||
"""
|
||||
assert "X-Request-ID" in ahriman_client.headers()
|
||||
|
||||
|
||||
def test_on_session_creation(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must log in user on start
|
||||
"""
|
||||
ahriman_client.auth = (user.username, user.password)
|
||||
requests_mock = mocker.patch("ahriman.core.http.SyncAhrimanClient.make_request")
|
||||
payload = {
|
||||
"username": user.username,
|
||||
"password": user.password
|
||||
}
|
||||
session = requests.Session()
|
||||
|
||||
ahriman_client.on_session_creation(session)
|
||||
requests_mock.assert_called_once_with("POST", pytest.helpers.anyvar(str, True), json=payload, session=session)
|
||||
|
||||
|
||||
def test_on_session_creation_failed(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress any exception happened during session start
|
||||
"""
|
||||
ahriman_client.user = user
|
||||
mocker.patch("requests.Session.request", side_effect=Exception)
|
||||
ahriman_client.on_session_creation(requests.Session())
|
||||
|
||||
|
||||
def test_start_failed_http_error(ahriman_client: SyncAhrimanClient, user: User, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must suppress HTTP exception happened during session start
|
||||
"""
|
||||
ahriman_client.user = user
|
||||
mocker.patch("requests.Session.request", side_effect=requests.HTTPError)
|
||||
ahriman_client.on_session_creation(requests.Session())
|
||||
|
||||
|
||||
def test_start_skip(ahriman_client: SyncAhrimanClient, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip login if no user set
|
||||
"""
|
||||
requests_mock = mocker.patch("requests.Session.request")
|
||||
ahriman_client.on_session_creation(requests.Session())
|
||||
requests_mock.assert_not_called()
|
||||
@@ -1,73 +0,0 @@
|
||||
import logging
|
||||
import pytest
|
||||
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.build_tools.task import Task
|
||||
from ahriman.core.database import SQLite
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_logger(database: SQLite, repo: Repo) -> None:
|
||||
"""
|
||||
must set logger attribute
|
||||
"""
|
||||
assert database.logger
|
||||
assert database.logger.name == "sql"
|
||||
|
||||
assert repo.logger
|
||||
assert repo.logger.name == "ahriman.core.alpm.repo.Repo"
|
||||
|
||||
|
||||
def test_logger_name(database: SQLite, repo: Repo, task_ahriman: Task) -> None:
|
||||
"""
|
||||
must correctly generate logger name
|
||||
"""
|
||||
assert database.logger_name == "sql"
|
||||
assert repo.logger_name == "ahriman.core.alpm.repo.Repo"
|
||||
assert task_ahriman.logger_name == "ahriman.core.build_tools.task.Task"
|
||||
|
||||
|
||||
def test_in_context(database: SQLite) -> None:
|
||||
"""
|
||||
must set and reset generic log context
|
||||
"""
|
||||
with database.in_context("package_id", "42"):
|
||||
record = logging.makeLogRecord({})
|
||||
assert record.package_id == "42"
|
||||
|
||||
record = logging.makeLogRecord({})
|
||||
assert not hasattr(record, "package_id")
|
||||
|
||||
|
||||
def test_in_context_failed(database: SQLite) -> None:
|
||||
"""
|
||||
must reset context even if exception occurs
|
||||
"""
|
||||
with pytest.raises(ValueError):
|
||||
with database.in_context("package_id", "42"):
|
||||
raise ValueError()
|
||||
|
||||
record = logging.makeLogRecord({})
|
||||
assert not hasattr(record, "package_id")
|
||||
|
||||
|
||||
def test_in_package_context(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must set package log context
|
||||
"""
|
||||
with database.in_package_context(package_ahriman.base, package_ahriman.version):
|
||||
record = logging.makeLogRecord({})
|
||||
assert record.package_id == LogRecordId(package_ahriman.base, package_ahriman.version)
|
||||
|
||||
record = logging.makeLogRecord({})
|
||||
assert not hasattr(record, "package_id")
|
||||
|
||||
|
||||
def test_in_package_context_empty_version(database: SQLite, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must set package log context with empty version
|
||||
"""
|
||||
with database.in_package_context(package_ahriman.base, None):
|
||||
record = logging.makeLogRecord({})
|
||||
assert record.package_id == LogRecordId(package_ahriman.base, "<unknown>")
|
||||
@@ -1,75 +0,0 @@
|
||||
import logging
|
||||
|
||||
from ahriman.core.log.log_context import LogContext
|
||||
|
||||
|
||||
def test_get() -> None:
|
||||
"""
|
||||
must get context variable value
|
||||
"""
|
||||
token = LogContext.set("package_id", "value")
|
||||
assert LogContext.get("package_id") == "value"
|
||||
LogContext.reset("package_id", token)
|
||||
|
||||
|
||||
def test_get_empty() -> None:
|
||||
"""
|
||||
must return None when context variable is unknown or not set
|
||||
"""
|
||||
assert LogContext.get("package_id") is None
|
||||
assert LogContext.get("random") is None
|
||||
|
||||
|
||||
def test_log_record_factory() -> None:
|
||||
"""
|
||||
must inject all registered context variables into log records
|
||||
"""
|
||||
package_token = LogContext.set("package_id", "package")
|
||||
|
||||
record = logging.makeLogRecord({})
|
||||
assert record.package_id == "package"
|
||||
|
||||
LogContext.reset("package_id", package_token)
|
||||
|
||||
|
||||
def test_log_record_factory_empty() -> None:
|
||||
"""
|
||||
must not inject context variable when value is None
|
||||
"""
|
||||
record = logging.makeLogRecord({})
|
||||
assert not hasattr(record, "package_id")
|
||||
|
||||
|
||||
def test_register() -> None:
|
||||
"""
|
||||
must register a context variable
|
||||
"""
|
||||
variable = LogContext.register("random")
|
||||
|
||||
assert "random" in LogContext._context
|
||||
assert LogContext._context["random"] is variable
|
||||
|
||||
del LogContext._context["random"]
|
||||
|
||||
|
||||
def test_reset() -> None:
|
||||
"""
|
||||
must reset context variable so it is no longer injected
|
||||
"""
|
||||
token = LogContext.set("package_id", "value")
|
||||
LogContext.reset("package_id", token)
|
||||
|
||||
record = logging.makeLogRecord({})
|
||||
assert not hasattr(record, "package_id")
|
||||
|
||||
|
||||
def test_set() -> None:
|
||||
"""
|
||||
must set context variable and inject it into log records
|
||||
"""
|
||||
token = LogContext.set("package_id", "value")
|
||||
|
||||
record = logging.makeLogRecord({})
|
||||
assert record.package_id == "value"
|
||||
|
||||
LogContext.reset("package_id", token)
|
||||
@@ -1,56 +0,0 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.repository import Explorer
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
|
||||
|
||||
def test_repositories_extract(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate list of available repositories based on arguments
|
||||
"""
|
||||
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
|
||||
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
|
||||
|
||||
assert Explorer.repositories_extract(configuration, "repo", "arch") == [RepositoryId("arch", "repo")]
|
||||
known_architectures_mock.assert_not_called()
|
||||
known_repositories_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_repositories_extract_repository(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate list of available repositories based on arguments and tree
|
||||
"""
|
||||
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
|
||||
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
|
||||
return_value={"repo"})
|
||||
|
||||
assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "repo")]
|
||||
known_architectures_mock.assert_not_called()
|
||||
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
|
||||
|
||||
|
||||
def test_repositories_extract_repository_legacy(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must generate list of available repositories based on arguments and tree (legacy mode)
|
||||
"""
|
||||
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures")
|
||||
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories",
|
||||
return_value=set())
|
||||
|
||||
assert Explorer.repositories_extract(configuration, architecture="arch") == [RepositoryId("arch", "aur")]
|
||||
known_architectures_mock.assert_not_called()
|
||||
known_repositories_mock.assert_called_once_with(configuration.repository_paths.root)
|
||||
|
||||
|
||||
def test_repositories_extract_architecture(configuration: Configuration, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must read repository name from config
|
||||
"""
|
||||
known_architectures_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_architectures",
|
||||
return_value={"arch"})
|
||||
known_repositories_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.known_repositories")
|
||||
|
||||
assert Explorer.repositories_extract(configuration, repository="repo") == [RepositoryId("arch", "repo")]
|
||||
known_architectures_mock.assert_called_once_with(configuration.repository_paths.root, "repo")
|
||||
known_repositories_mock.assert_not_called()
|
||||
@@ -1,268 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from ahriman.core.repository import Repository
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_full_depends(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
pyalpm_package_ahriman: MagicMock) -> None:
|
||||
"""
|
||||
must extract all dependencies from the package
|
||||
"""
|
||||
package_python_schedule.packages[package_python_schedule.base].provides = ["python3-schedule"]
|
||||
|
||||
database_mock = MagicMock()
|
||||
database_mock.pkgcache = [pyalpm_package_ahriman]
|
||||
repository.pacman = MagicMock()
|
||||
repository.pacman.handle.get_syncdbs.return_value = [database_mock]
|
||||
|
||||
assert repository.full_depends(package_ahriman, [package_python_schedule]) == package_ahriman.depends
|
||||
|
||||
package_python_schedule.packages[package_python_schedule.base].depends = [package_ahriman.base]
|
||||
expected = sorted(set(package_python_schedule.depends + package_ahriman.depends))
|
||||
assert repository.full_depends(package_python_schedule, [package_python_schedule]) == expected
|
||||
|
||||
|
||||
def test_load_archives(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return all packages grouped by package base
|
||||
"""
|
||||
single_packages = [
|
||||
Package(base=package_python_schedule.base,
|
||||
version=package_python_schedule.version,
|
||||
remote=package_python_schedule.remote,
|
||||
packages={package: props})
|
||||
for package, props in package_python_schedule.packages.items()
|
||||
] + [package_ahriman]
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages)
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[
|
||||
(package_ahriman, None),
|
||||
])
|
||||
|
||||
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz"), Path("c.pkg.tar.xz")])
|
||||
assert len(packages) == 2
|
||||
assert {package.base for package in packages} == {package_ahriman.base, package_python_schedule.base}
|
||||
|
||||
archives = sum((list(package.packages.keys()) for package in packages), start=[])
|
||||
assert len(archives) == 3
|
||||
expected = set(package_ahriman.packages.keys())
|
||||
expected.update(package_python_schedule.packages.keys())
|
||||
assert set(archives) == expected
|
||||
|
||||
|
||||
def test_load_archives_failed(repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip packages which cannot be loaded
|
||||
"""
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=Exception)
|
||||
assert not repository.load_archives([Path("a.pkg.tar.xz")])
|
||||
|
||||
|
||||
def test_load_archives_not_package(repository: Repository) -> None:
|
||||
"""
|
||||
must skip not packages from iteration
|
||||
"""
|
||||
assert not repository.load_archives([Path("a.tar.xz")])
|
||||
|
||||
|
||||
def test_load_archives_different_version(repository: Repository, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load packages with different versions choosing maximal
|
||||
"""
|
||||
single_packages = [
|
||||
Package(base=package_python_schedule.base,
|
||||
version=package_python_schedule.version,
|
||||
remote=package_python_schedule.remote,
|
||||
packages={package: props})
|
||||
for package, props in package_python_schedule.packages.items()
|
||||
]
|
||||
single_packages[0].version = "0.0.1-1"
|
||||
mocker.patch("ahriman.models.package.Package.from_archive", side_effect=single_packages)
|
||||
|
||||
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")])
|
||||
assert len(packages) == 1
|
||||
assert packages[0].version == package_python_schedule.version
|
||||
|
||||
|
||||
def test_load_archives_all_versions(repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load packages with different versions keeping all when latest_only is False
|
||||
"""
|
||||
mocker.patch("ahriman.models.package.Package.from_archive",
|
||||
side_effect=[package_ahriman, replace(package_ahriman, version="0.0.1-1")])
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[])
|
||||
|
||||
packages = repository.load_archives([Path("a.pkg.tar.xz"), Path("b.pkg.tar.xz")], latest_only=False)
|
||||
assert len(packages) == 2
|
||||
|
||||
|
||||
def test_package_archives(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load package archives sorted by version
|
||||
"""
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[replace(package_ahriman, version=str(i)) for i in range(5)])
|
||||
|
||||
result = repository.package_archives(package_ahriman.base)
|
||||
assert len(result) == 5
|
||||
assert [p.version for p in result] == [str(i) for i in range(5)]
|
||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int), latest_only=False)
|
||||
|
||||
|
||||
def test_package_archives_no_directory(repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return empty list if archive directory does not exist
|
||||
"""
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=False)
|
||||
assert repository.package_archives(package_ahriman.base) == []
|
||||
|
||||
|
||||
def test_package_archives_architecture_mismatch(repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip packages with mismatched architecture
|
||||
"""
|
||||
package_ahriman.packages[package_ahriman.base].architecture = "i686"
|
||||
|
||||
mocker.patch("pathlib.Path.is_dir", return_value=True)
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman])
|
||||
|
||||
result = repository.package_archives(package_ahriman.base)
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
def test_package_archives_lookup(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must existing packages which match the version
|
||||
"""
|
||||
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[
|
||||
package_ahriman,
|
||||
package_python_schedule,
|
||||
replace(package_ahriman, version="1"),
|
||||
])
|
||||
glob_mock = mocker.patch("pathlib.Path.glob", return_value=[Path("1.pkg.tar.xz")])
|
||||
|
||||
assert repository.package_archives_lookup(package_ahriman) == [Path("1.pkg.tar.xz")]
|
||||
archives_mock.assert_called_once_with(package_ahriman.base)
|
||||
glob_mock.assert_called_once_with(f"{package_ahriman.packages[package_ahriman.base].filename}*")
|
||||
|
||||
|
||||
def test_package_archives_lookup_version_mismatch(repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return nothing if no packages found with the same version
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
|
||||
return_value=[replace(package_ahriman, version="1")])
|
||||
assert repository.package_archives_lookup(package_ahriman) == []
|
||||
|
||||
|
||||
def test_package_archives_lookup_architecture_mismatch(repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return nothing if architecture doesn't match
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
|
||||
assert repository.package_archives_lookup(package_ahriman) == []
|
||||
|
||||
|
||||
def test_package_archives_lookup_no_archive_directory(repository: Repository, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return nothing if no archive directory found
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives", return_value=[])
|
||||
assert repository.package_archives_lookup(package_ahriman) == []
|
||||
|
||||
|
||||
def test_package_changes(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must load package changes
|
||||
"""
|
||||
changes = Changes("sha", "change")
|
||||
load_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha2")
|
||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes", return_value=changes)
|
||||
|
||||
assert repository.package_changes(package_ahriman, changes.last_commit_sha) == changes
|
||||
load_mock.assert_called_once_with(
|
||||
pytest.helpers.anyvar(int), package_ahriman, [], repository.configuration.repository_paths)
|
||||
changes_mock.assert_called_once_with(pytest.helpers.anyvar(int), changes.last_commit_sha)
|
||||
|
||||
|
||||
def test_package_changes_skip(repository: Repository, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip loading package changes if no new commits
|
||||
"""
|
||||
mocker.patch("ahriman.core.build_tools.sources.Sources.load", return_value="sha")
|
||||
changes_mock = mocker.patch("ahriman.core.build_tools.sources.Sources.changes")
|
||||
|
||||
assert repository.package_changes(package_ahriman, "sha") is None
|
||||
changes_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_packages(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return repository packages
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
load_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert repository.packages() == [package_ahriman, package_python_schedule]
|
||||
# it uses filter object, so we cannot verify argument list =/
|
||||
load_mock.assert_called_once_with(pytest.helpers.anyvar(int))
|
||||
|
||||
|
||||
def test_packages_filter(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter result by bases
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir")
|
||||
mocker.patch("ahriman.core.repository.package_info.PackageInfo.load_archives",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert repository.packages([package_ahriman.base]) == [package_ahriman]
|
||||
|
||||
|
||||
def test_packages_built(repository: Repository, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return build packages
|
||||
"""
|
||||
mocker.patch("pathlib.Path.iterdir", return_value=[Path("a.tar.xz"), Path("b.pkg.tar.xz")])
|
||||
assert repository.packages_built() == [Path("b.pkg.tar.xz")]
|
||||
|
||||
|
||||
def test_packages_depend_on(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must filter packages by depends list
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert repository.packages_depend_on([package_ahriman], {"python-srcinfo"}) == [package_ahriman]
|
||||
|
||||
|
||||
def test_packages_depend_on_empty(repository: Repository, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return all packages in case if no filter is provided
|
||||
"""
|
||||
mocker.patch("ahriman.core.repository.repository.Repository.packages",
|
||||
return_value=[package_ahriman, package_python_schedule])
|
||||
assert repository.packages_depend_on([package_ahriman, package_python_schedule], None) == \
|
||||
[package_ahriman, package_python_schedule]
|
||||
@@ -1,145 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from asyncio import QueueShutDown
|
||||
|
||||
from ahriman.core.status.event_bus import EventBus
|
||||
from ahriman.models.event import EventType
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
async def test_broadcast(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must broadcast event to all subscribers
|
||||
"""
|
||||
_, queue = await event_bus.subscribe()
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base, version=package_ahriman.version)
|
||||
|
||||
message = queue.get_nowait()
|
||||
assert message == (
|
||||
EventType.PackageUpdated,
|
||||
{"object_id": package_ahriman.base, "version": package_ahriman.version},
|
||||
)
|
||||
|
||||
|
||||
async def test_broadcast_with_topics(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must broadcast event to subscribers with matching topics
|
||||
"""
|
||||
_, queue = await event_bus.subscribe([EventType.PackageUpdated])
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
assert not queue.empty()
|
||||
|
||||
|
||||
async def test_broadcast_topic_isolation(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must not broadcast event to subscribers with non-matching topics
|
||||
"""
|
||||
_, queue = await event_bus.subscribe([EventType.BuildLog])
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
assert queue.empty()
|
||||
|
||||
|
||||
async def test_broadcast_queue_full(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must discard message to slow subscriber
|
||||
"""
|
||||
event_bus.max_size = 1
|
||||
_, queue = await event_bus.subscribe()
|
||||
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
await event_bus.broadcast(EventType.PackageRemoved, package_ahriman.base)
|
||||
assert queue.qsize() == 1
|
||||
|
||||
|
||||
async def test_broadcast_queue_shutdown(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must skip subscriber whose queue was shutdown concurrently
|
||||
"""
|
||||
_, queue = await event_bus.subscribe()
|
||||
queue.shutdown()
|
||||
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
|
||||
|
||||
async def test_shutdown(event_bus: EventBus) -> None:
|
||||
"""
|
||||
must shutdown all subscriber queues on shutdown
|
||||
"""
|
||||
subscriber_id, queue = await event_bus.subscribe()
|
||||
|
||||
await event_bus.shutdown()
|
||||
assert subscriber_id not in event_bus._subscribers
|
||||
with pytest.raises(QueueShutDown):
|
||||
queue.get_nowait()
|
||||
|
||||
|
||||
async def test_shutdown_queue_full(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must handle shutdown when queue is full
|
||||
"""
|
||||
event_bus.max_size = 1
|
||||
_, queue = await event_bus.subscribe()
|
||||
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
await event_bus.shutdown()
|
||||
|
||||
|
||||
async def test_subscribe(event_bus: EventBus) -> None:
|
||||
"""
|
||||
must register new subscriber
|
||||
"""
|
||||
subscriber_id, queue = await event_bus.subscribe()
|
||||
|
||||
assert subscriber_id
|
||||
assert queue.empty()
|
||||
assert subscriber_id in event_bus._subscribers
|
||||
|
||||
|
||||
async def test_broadcast_with_object_id(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must broadcast event to subscribers with matching object_id
|
||||
"""
|
||||
_, queue = await event_bus.subscribe(object_id=package_ahriman.base)
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
assert not queue.empty()
|
||||
|
||||
|
||||
async def test_broadcast_object_id_isolation(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must not broadcast event to subscribers with non-matching object_id
|
||||
"""
|
||||
_, queue = await event_bus.subscribe(object_id="other-package")
|
||||
await event_bus.broadcast(EventType.PackageUpdated, package_ahriman.base)
|
||||
assert queue.empty()
|
||||
|
||||
|
||||
async def test_subscribe_with_topics(event_bus: EventBus) -> None:
|
||||
"""
|
||||
must register subscriber with topic filter
|
||||
"""
|
||||
subscriber_id, _ = await event_bus.subscribe([EventType.BuildLog])
|
||||
assert event_bus._subscribers[subscriber_id].topics == [EventType.BuildLog]
|
||||
|
||||
|
||||
async def test_subscribe_with_object_id(event_bus: EventBus, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must register subscriber with object_id filter
|
||||
"""
|
||||
subscriber_id, _ = await event_bus.subscribe(object_id=package_ahriman.base)
|
||||
assert event_bus._subscribers[subscriber_id].object_id == package_ahriman.base
|
||||
|
||||
|
||||
async def test_unsubscribe(event_bus: EventBus) -> None:
|
||||
"""
|
||||
must remove subscriber
|
||||
"""
|
||||
subscriber_id, _ = await event_bus.subscribe()
|
||||
await event_bus.unsubscribe(subscriber_id)
|
||||
assert subscriber_id not in event_bus._subscribers
|
||||
|
||||
|
||||
async def test_unsubscribe_unknown(event_bus: EventBus) -> None:
|
||||
"""
|
||||
must not fail on unknown subscriber removal
|
||||
"""
|
||||
await event_bus.unsubscribe("unknown")
|
||||
@@ -1,385 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.exceptions import UnknownPackageError
|
||||
from ahriman.core.status.watcher import Watcher
|
||||
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
|
||||
from ahriman.models.changes import Changes
|
||||
from ahriman.models.dependencies import Dependencies
|
||||
from ahriman.models.event import Event, EventType
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.pkgbuild_patch import PkgbuildPatch
|
||||
|
||||
|
||||
async def test_event_add(watcher: Watcher, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create new event
|
||||
"""
|
||||
event = Event("event", "object")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_add")
|
||||
|
||||
await watcher.event_add(event)
|
||||
cache_mock.assert_called_once_with(event)
|
||||
|
||||
|
||||
async def test_event_get(watcher: Watcher, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must retrieve events
|
||||
"""
|
||||
event = Event("event", "object")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.event_get", return_value=[event])
|
||||
|
||||
result = await watcher.event_get(None, None)
|
||||
assert result == [event]
|
||||
cache_mock.assert_called_once_with(None, None, None, None, -1, 0)
|
||||
|
||||
|
||||
async def test_load(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly load packages
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_get",
|
||||
return_value=[(package_ahriman, BuildStatus())])
|
||||
|
||||
await watcher.load()
|
||||
cache_mock.assert_called_once_with(None)
|
||||
package, status = watcher._known[package_ahriman.base]
|
||||
assert package == package_ahriman
|
||||
assert status.status == BuildStatusEnum.Unknown
|
||||
|
||||
|
||||
async def test_load_known(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must correctly load packages with known statuses
|
||||
"""
|
||||
status = BuildStatus(BuildStatusEnum.Success)
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_get", return_value=[(package_ahriman, status)])
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, status)}
|
||||
|
||||
await watcher.load()
|
||||
_, status = watcher._known[package_ahriman.base]
|
||||
assert status.status == BuildStatusEnum.Success
|
||||
|
||||
|
||||
async def test_logs_rotate(watcher: Watcher, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must rotate logs
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.logs_rotate")
|
||||
await watcher.logs_rotate(42)
|
||||
cache_mock.assert_called_once_with(42)
|
||||
|
||||
|
||||
async def test_package_archives(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return package archives from package info
|
||||
"""
|
||||
archives_mock = mocker.patch("ahriman.core.repository.package_info.PackageInfo.package_archives",
|
||||
return_value=[package_ahriman])
|
||||
|
||||
result = await watcher.package_archives(package_ahriman.base)
|
||||
assert result == [package_ahriman]
|
||||
archives_mock.assert_called_once_with(package_ahriman.base)
|
||||
|
||||
|
||||
async def test_package_get(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must return package status
|
||||
"""
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
package, status = await watcher.package_get(package_ahriman.base)
|
||||
assert package == package_ahriman
|
||||
assert status.status == BuildStatusEnum.Unknown
|
||||
|
||||
|
||||
async def test_package_get_failed(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must fail on unknown package
|
||||
"""
|
||||
with pytest.raises(UnknownPackageError):
|
||||
await watcher.package_get(package_ahriman.base)
|
||||
|
||||
|
||||
async def test_package_changes_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return package changes
|
||||
"""
|
||||
changes = Changes("sha")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_get",
|
||||
return_value=changes)
|
||||
|
||||
assert await watcher.package_changes_get(package_ahriman.base) == changes
|
||||
cache_mock.assert_called_once_with(package_ahriman.base)
|
||||
|
||||
|
||||
async def test_package_changes_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update package changes
|
||||
"""
|
||||
changes = Changes("sha")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_changes_update")
|
||||
|
||||
await watcher.package_changes_update(package_ahriman.base, changes)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, changes)
|
||||
|
||||
|
||||
async def test_package_dependencies_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return package dependencies
|
||||
"""
|
||||
dependencies = Dependencies({"path": [package_ahriman.base]})
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_get",
|
||||
return_value=dependencies)
|
||||
|
||||
assert await watcher.package_dependencies_get(package_ahriman.base) == dependencies
|
||||
cache_mock.assert_called_once_with(package_ahriman.base)
|
||||
|
||||
|
||||
async def test_package_dependencies_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update package dependencies
|
||||
"""
|
||||
dependencies = Dependencies({"path": [package_ahriman.base]})
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_dependencies_update")
|
||||
|
||||
await watcher.package_dependencies_update(package_ahriman.base, dependencies)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, dependencies)
|
||||
|
||||
|
||||
async def test_package_hold_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update package hold status
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_hold_update")
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
|
||||
await watcher.package_hold_update(package_ahriman.base, enabled=True)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, enabled=True)
|
||||
_, status = watcher._known[package_ahriman.base]
|
||||
assert status.is_held is True
|
||||
broadcast_mock.assert_called_once_with(EventType.PackageHeld, package_ahriman.base, is_held=True)
|
||||
|
||||
|
||||
async def test_package_hold_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must fail on unknown package hold update
|
||||
"""
|
||||
with pytest.raises(UnknownPackageError):
|
||||
await watcher.package_hold_update(package_ahriman.base, enabled=True)
|
||||
|
||||
|
||||
async def test_package_logs_add(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must post log record
|
||||
"""
|
||||
log_record = LogRecord(LogRecordId(package_ahriman.base, "1.0.0"), 42.0, "message")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_add")
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
|
||||
await watcher.package_logs_add(log_record)
|
||||
cache_mock.assert_called_once_with(log_record)
|
||||
broadcast_mock.assert_called_once_with(EventType.BuildLog, package_ahriman.base, **log_record.view())
|
||||
|
||||
|
||||
async def test_package_logs_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return package logs
|
||||
"""
|
||||
log_record = LogRecord(LogRecordId(package_ahriman.base, "1.0.0"), 42.0, "message")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_get",
|
||||
return_value=[log_record])
|
||||
|
||||
assert await watcher.package_logs_get(package_ahriman.base) == [log_record]
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, None, None, -1, 0)
|
||||
|
||||
|
||||
async def test_package_logs_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove package logs
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_logs_remove")
|
||||
await watcher.package_logs_remove(package_ahriman.base, None)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, None)
|
||||
|
||||
|
||||
async def test_package_patches_get(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must return package patches
|
||||
"""
|
||||
patch = PkgbuildPatch("key", "value")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_get", return_value=[patch])
|
||||
|
||||
assert await watcher.package_patches_get(package_ahriman.base, None) == [patch]
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, None)
|
||||
|
||||
|
||||
async def test_package_patches_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove package patches
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_remove")
|
||||
await watcher.package_patches_remove(package_ahriman.base, None)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, None)
|
||||
|
||||
|
||||
async def test_package_patches_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update package patches
|
||||
"""
|
||||
patch = PkgbuildPatch("key", "value")
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_patches_update")
|
||||
|
||||
await watcher.package_patches_update(package_ahriman.base, patch)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, patch)
|
||||
|
||||
|
||||
async def test_package_remove(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove package base
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
|
||||
await watcher.package_remove(package_ahriman.base)
|
||||
assert not watcher._known
|
||||
cache_mock.assert_called_once_with(package_ahriman.base)
|
||||
broadcast_mock.assert_called_once_with(EventType.PackageRemoved, package_ahriman.base)
|
||||
|
||||
|
||||
async def test_package_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must not fail on unknown base removal
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
|
||||
await watcher.package_remove(package_ahriman.base)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base)
|
||||
broadcast_mock.assert_called_once_with(EventType.PackageRemoved, package_ahriman.base)
|
||||
|
||||
|
||||
async def test_package_status_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update package status only for known package
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_status_update")
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
|
||||
await watcher.package_status_update(package_ahriman.base, BuildStatusEnum.Success)
|
||||
cache_mock.assert_called_once_with(package_ahriman.base, pytest.helpers.anyvar(int))
|
||||
package, status = watcher._known[package_ahriman.base]
|
||||
assert package == package_ahriman
|
||||
assert status.status == BuildStatusEnum.Success
|
||||
broadcast_mock.assert_called_once_with(
|
||||
EventType.PackageStatusChanged, package_ahriman.base, status=BuildStatusEnum.Success.value,
|
||||
)
|
||||
|
||||
|
||||
async def test_package_status_update_preserves_hold(watcher: Watcher, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must preserve hold status on package status update
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_status_update")
|
||||
mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus(is_held=True))}
|
||||
|
||||
await watcher.package_status_update(package_ahriman.base, BuildStatusEnum.Success)
|
||||
_, status = watcher._known[package_ahriman.base]
|
||||
assert status.is_held is True
|
||||
|
||||
|
||||
async def test_package_status_update_unknown(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must fail on unknown package status update only
|
||||
"""
|
||||
with pytest.raises(UnknownPackageError):
|
||||
await watcher.package_status_update(package_ahriman.base, BuildStatusEnum.Unknown)
|
||||
|
||||
|
||||
async def test_package_update(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must add package to cache
|
||||
"""
|
||||
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_update")
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
|
||||
await watcher.package_update(package_ahriman, BuildStatusEnum.Unknown)
|
||||
assert await watcher.packages()
|
||||
cache_mock.assert_called_once_with(package_ahriman, pytest.helpers.anyvar(int))
|
||||
broadcast_mock.assert_called_once_with(
|
||||
EventType.PackageUpdated, package_ahriman.base,
|
||||
status=BuildStatusEnum.Unknown.value, version=package_ahriman.version,
|
||||
)
|
||||
|
||||
|
||||
async def test_package_update_preserves_hold(watcher: Watcher, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must preserve hold status on package update
|
||||
"""
|
||||
mocker.patch("ahriman.core.status.local_client.LocalClient.package_update")
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus(is_held=True))}
|
||||
|
||||
await watcher.package_update(package_ahriman, BuildStatusEnum.Success)
|
||||
_, status = watcher._known[package_ahriman.base]
|
||||
assert status.is_held is True
|
||||
|
||||
|
||||
async def test_packages(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must return list of available packages
|
||||
"""
|
||||
assert not await watcher.packages()
|
||||
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
assert await watcher.packages()
|
||||
|
||||
|
||||
async def test_shutdown(watcher: Watcher, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must gracefully shutdown watcher
|
||||
"""
|
||||
shutdown_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.shutdown")
|
||||
await watcher.shutdown()
|
||||
shutdown_mock.assert_called_once_with()
|
||||
|
||||
|
||||
async def test_status_update(watcher: Watcher, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must update service status
|
||||
"""
|
||||
broadcast_mock = mocker.patch("ahriman.core.status.event_bus.EventBus.broadcast")
|
||||
|
||||
await watcher.status_update(BuildStatusEnum.Success)
|
||||
assert watcher.status.status == BuildStatusEnum.Success
|
||||
broadcast_mock.assert_called_once_with(EventType.ServiceStatusChanged, None, status=BuildStatusEnum.Success.value)
|
||||
|
||||
|
||||
def test_call(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must return self instance if package exists
|
||||
"""
|
||||
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
|
||||
assert watcher(package_ahriman.base)
|
||||
|
||||
|
||||
def test_call_skip(watcher: Watcher) -> None:
|
||||
"""
|
||||
must return self instance if no package base set
|
||||
"""
|
||||
assert watcher(None)
|
||||
|
||||
|
||||
def test_call_failed(watcher: Watcher, package_ahriman: Package) -> None:
|
||||
"""
|
||||
must raise UnknownPackage
|
||||
"""
|
||||
with pytest.raises(UnknownPackageError):
|
||||
assert watcher(package_ahriman.base)
|
||||
@@ -1,13 +0,0 @@
|
||||
from ahriman.models.log_record import LogRecord
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
|
||||
|
||||
def test_log_record_from_json_view() -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
log_record = LogRecord(LogRecordId("base", "version"), 0, "message")
|
||||
assert LogRecord.from_json(log_record.log_record_id.package_base, log_record.view()) == log_record
|
||||
|
||||
log_record = LogRecord(LogRecordId("base", "version", "process_id"), 0, "message")
|
||||
assert LogRecord.from_json(log_record.log_record_id.package_base, log_record.view()) == log_record
|
||||
@@ -1,9 +0,0 @@
|
||||
from ahriman.models.log_record_id import LogRecordId
|
||||
|
||||
|
||||
def test_init() -> None:
|
||||
"""
|
||||
must correctly assign process identifier if not set
|
||||
"""
|
||||
assert LogRecordId("1", "2").process_id == LogRecordId.DEFAULT_PROCESS_ID
|
||||
assert LogRecordId("1", "2", "3").process_id == "3"
|
||||
@@ -1,24 +0,0 @@
|
||||
from dataclasses import asdict
|
||||
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_stats import RepositoryStats
|
||||
|
||||
|
||||
def test_repository_stats_from_json_view(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must construct same object from json
|
||||
"""
|
||||
stats = RepositoryStats.from_packages([package_ahriman, package_python_schedule])
|
||||
assert RepositoryStats.from_json(asdict(stats)) == stats
|
||||
|
||||
|
||||
def test_from_packages(package_ahriman: Package, package_python_schedule: Package) -> None:
|
||||
"""
|
||||
must generate stats from packages list
|
||||
"""
|
||||
assert RepositoryStats.from_packages([package_ahriman, package_python_schedule]) == RepositoryStats(
|
||||
bases=2,
|
||||
packages=3,
|
||||
archive_size=12603,
|
||||
installed_size=12600003,
|
||||
)
|
||||
@@ -1,80 +0,0 @@
|
||||
from ahriman.models.series_statistics import SeriesStatistics
|
||||
|
||||
|
||||
def test_max() -> None:
|
||||
"""
|
||||
must return maximal value
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).max == 3
|
||||
|
||||
|
||||
def test_max_empty() -> None:
|
||||
"""
|
||||
must return None as maximal value if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).max is None
|
||||
|
||||
|
||||
def test_mean() -> None:
|
||||
"""
|
||||
must return mean value
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).mean == 2
|
||||
|
||||
|
||||
def test_mean_empty() -> None:
|
||||
"""
|
||||
must return None as mean value if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).mean is None
|
||||
|
||||
|
||||
def test_min() -> None:
|
||||
"""
|
||||
must return minimal value
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).min == 1
|
||||
|
||||
|
||||
def test_min_empty() -> None:
|
||||
"""
|
||||
must return None as minimal value if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).min is None
|
||||
|
||||
|
||||
def test_st_dev() -> None:
|
||||
"""
|
||||
must return standard deviation
|
||||
"""
|
||||
assert SeriesStatistics([1, 3, 2]).st_dev == 1
|
||||
|
||||
|
||||
def test_st_dev_empty() -> None:
|
||||
"""
|
||||
must return None as standard deviation if series is empty
|
||||
"""
|
||||
assert SeriesStatistics([]).st_dev is None
|
||||
|
||||
|
||||
def test_st_dev_single() -> None:
|
||||
"""
|
||||
must return 0 as standard deviation if series contains only one element
|
||||
"""
|
||||
assert SeriesStatistics([1]).st_dev == 0
|
||||
|
||||
|
||||
def test_total() -> None:
|
||||
"""
|
||||
must return size of collection
|
||||
"""
|
||||
assert SeriesStatistics([1]).total == 1
|
||||
assert SeriesStatistics([]).total == 0
|
||||
|
||||
|
||||
def test_bool() -> None:
|
||||
"""
|
||||
must correctly define empty collection
|
||||
"""
|
||||
assert SeriesStatistics([1])
|
||||
assert not SeriesStatistics([])
|
||||
@@ -1,162 +0,0 @@
|
||||
import datetime
|
||||
import pytest
|
||||
|
||||
from unittest.mock import MagicMock, PropertyMock
|
||||
|
||||
from ahriman.core.alpm.pacman import Pacman
|
||||
from ahriman.core.alpm.remote import AUR
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.models.aur_package import AURPackage
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.package_source import PackageSource
|
||||
from ahriman.models.pacman_synchronization import PacmanSynchronization
|
||||
from ahriman.models.remote_source import RemoteSource
|
||||
from ahriman.models.scan_paths import ScanPaths
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def aur_package_akonadi() -> AURPackage:
|
||||
"""
|
||||
fixture for AUR package
|
||||
|
||||
Returns:
|
||||
AURPackage: AUR package test instance
|
||||
"""
|
||||
return AURPackage(
|
||||
id=0,
|
||||
name="akonadi",
|
||||
package_base_id=0,
|
||||
package_base="akonadi",
|
||||
version="21.12.3-2",
|
||||
description="PIM layer, which provides an asynchronous API to access all kind of PIM data",
|
||||
num_votes=0,
|
||||
popularity=0.0,
|
||||
first_submitted=datetime.datetime.fromtimestamp(0, datetime.UTC),
|
||||
last_modified=datetime.datetime.fromtimestamp(1646555990.610, datetime.UTC),
|
||||
url_path="",
|
||||
url="https://kontact.kde.org",
|
||||
out_of_date=None,
|
||||
maintainer="felixonmars",
|
||||
repository="extra",
|
||||
depends=[
|
||||
"libakonadi",
|
||||
"mariadb",
|
||||
],
|
||||
make_depends=[
|
||||
"boost",
|
||||
"doxygen",
|
||||
"extra-cmake-modules",
|
||||
"kaccounts-integration",
|
||||
"kitemmodels",
|
||||
"postgresql",
|
||||
"qt5-tools",
|
||||
],
|
||||
opt_depends=[
|
||||
"postgresql: PostgreSQL backend",
|
||||
],
|
||||
conflicts=[],
|
||||
provides=[],
|
||||
license=["LGPL"],
|
||||
keywords=[],
|
||||
groups=[],
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def package_tpacpi_bat_git() -> Package:
|
||||
"""
|
||||
git package fixture
|
||||
|
||||
Returns:
|
||||
Package: git package test instance
|
||||
"""
|
||||
return Package(
|
||||
base="tpacpi-bat-git",
|
||||
version="3.1.r12.g4959b52-1",
|
||||
remote=RemoteSource(
|
||||
source=PackageSource.AUR,
|
||||
git_url=AUR.remote_git_url("tpacpi-bat-git", "aur"),
|
||||
web_url=AUR.remote_web_url("tpacpi-bat-git"),
|
||||
path=".",
|
||||
branch="master",
|
||||
),
|
||||
packages={"tpacpi-bat-git": PackageDescription()})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pacman(configuration: Configuration) -> Pacman:
|
||||
"""
|
||||
fixture for pacman wrapper
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
Pacman: pacman wrapper test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return Pacman(repository_id, configuration, refresh_database=PacmanSynchronization.Disabled)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def passwd() -> MagicMock:
|
||||
"""
|
||||
get passwd structure for the user
|
||||
|
||||
Returns:
|
||||
MagicMock: passwd structure test instance
|
||||
"""
|
||||
passwd = MagicMock()
|
||||
passwd.pw_dir = "home"
|
||||
passwd.pw_name = "ahriman"
|
||||
return passwd
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pyalpm_package_ahriman(aur_package_ahriman: AURPackage) -> MagicMock:
|
||||
"""
|
||||
mock object for pyalpm package
|
||||
|
||||
Args:
|
||||
aur_package_ahriman(AURPackage): package fixture
|
||||
|
||||
Returns:
|
||||
MagicMock: pyalpm package mock
|
||||
"""
|
||||
mock = MagicMock()
|
||||
db = type(mock).db = MagicMock()
|
||||
|
||||
type(mock).base = PropertyMock(return_value=aur_package_ahriman.package_base)
|
||||
type(mock).builddate = PropertyMock(
|
||||
return_value=aur_package_ahriman.last_modified.replace(tzinfo=datetime.timezone.utc).timestamp())
|
||||
type(mock).conflicts = PropertyMock(return_value=aur_package_ahriman.conflicts)
|
||||
type(db).name = PropertyMock(return_value="aur")
|
||||
type(mock).depends = PropertyMock(return_value=aur_package_ahriman.depends)
|
||||
type(mock).desc = PropertyMock(return_value=aur_package_ahriman.description)
|
||||
type(mock).licenses = PropertyMock(return_value=aur_package_ahriman.license)
|
||||
type(mock).makedepends = PropertyMock(return_value=aur_package_ahriman.make_depends)
|
||||
type(mock).name = PropertyMock(return_value=aur_package_ahriman.name)
|
||||
type(mock).optdepends = PropertyMock(return_value=aur_package_ahriman.opt_depends)
|
||||
type(mock).checkdepends = PropertyMock(return_value=aur_package_ahriman.check_depends)
|
||||
type(mock).packager = PropertyMock(return_value="packager")
|
||||
type(mock).provides = PropertyMock(return_value=aur_package_ahriman.provides)
|
||||
type(mock).version = PropertyMock(return_value=aur_package_ahriman.version)
|
||||
type(mock).url = PropertyMock(return_value=aur_package_ahriman.url)
|
||||
type(mock).groups = PropertyMock(return_value=aur_package_ahriman.groups)
|
||||
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scan_paths(configuration: Configuration) -> ScanPaths:
|
||||
"""
|
||||
scan paths fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration test instance
|
||||
|
||||
Returns:
|
||||
ScanPaths: scan paths test instance
|
||||
"""
|
||||
return ScanPaths(configuration.getlist("build", "scan_paths", fallback=[]))
|
||||
@@ -1,32 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ahriman-triggers"
|
||||
description = "ArcH linux ReposItory MANager, additional extensions"
|
||||
readme = "../README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = {file = "../COPYING"}
|
||||
authors = [
|
||||
{name = "ahriman team"},
|
||||
]
|
||||
dependencies = [
|
||||
"ahriman-core",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://ahriman.readthedocs.io/"
|
||||
Repository = "https://github.com/arcan1s/ahriman"
|
||||
Changelog = "https://github.com/arcan1s/ahriman/releases"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "../ahriman-core/src/ahriman/__init__.py"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
only-include = ["src/ahriman"]
|
||||
sources = ["src"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.shared-data]
|
||||
"package/share" = "share"
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
@@ -1,185 +0,0 @@
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
import datetime
|
||||
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
from ahriman.core.alpm.repo import Repo
|
||||
from ahriman.core.log import LazyLogging
|
||||
from ahriman.core.utils import package_like, symlink_relative, utcnow, walk
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.package_description import PackageDescription
|
||||
from ahriman.models.repository_paths import RepositoryPaths
|
||||
|
||||
|
||||
class ArchiveTree(LazyLogging):
|
||||
"""
|
||||
wrapper around archive tree
|
||||
|
||||
Attributes:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
|
||||
def __init__(self, repository_path: RepositoryPaths, sign_args: list[str]) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_path(RepositoryPaths): repository paths instance
|
||||
sign_args(list[str]): additional args which have to be used to sign repository archive
|
||||
"""
|
||||
self.paths = repository_path
|
||||
self.repository_id = repository_path.repository_id
|
||||
self.sign_args = sign_args
|
||||
|
||||
@staticmethod
|
||||
def _package_symlinks_create(package_description: PackageDescription, root: Path, archive: Path) -> bool:
|
||||
"""
|
||||
process symlinks creation for single package
|
||||
|
||||
Args:
|
||||
package_description(PackageDescription): archive descriptor
|
||||
root(Path): path to the archive repository root
|
||||
archive(Path): path to directory with archives
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if symlinks were created and ``False`` otherwise
|
||||
"""
|
||||
symlinks_created = False
|
||||
# here we glob for archive itself and signature if any
|
||||
for file in archive.glob(f"{package_description.filename}*"):
|
||||
try:
|
||||
symlink_relative(root / file.name, file)
|
||||
symlinks_created = True
|
||||
except FileExistsError:
|
||||
continue # symlink is already created, skip processing
|
||||
|
||||
return symlinks_created
|
||||
|
||||
def _repo(self, root: Path) -> Repo:
|
||||
"""
|
||||
constructs :class:`ahriman.core.alpm.repo.Repo` object for given path
|
||||
|
||||
Args:
|
||||
root(Path): root of the repository
|
||||
|
||||
Returns:
|
||||
Repo: constructed object with correct properties
|
||||
"""
|
||||
return Repo(self.repository_id.name, self.paths, self.sign_args, root)
|
||||
|
||||
def directories_fix(self, paths: set[Path]) -> None:
|
||||
"""
|
||||
remove empty repository directories recursively
|
||||
|
||||
Args:
|
||||
paths(set[Path]): repositories to check
|
||||
"""
|
||||
root = self.paths.archive / "repos"
|
||||
for repository in paths:
|
||||
parents = [repository] + list(repository.parents[:-1])
|
||||
for parent in parents:
|
||||
path = root / parent
|
||||
if list(path.iterdir()):
|
||||
continue # directory is not empty
|
||||
path.rmdir()
|
||||
|
||||
def repository_for(self, date: datetime.date | None = None) -> Path:
|
||||
"""
|
||||
get full path to repository at the specified date
|
||||
|
||||
Args:
|
||||
date(datetime.date | None, optional): date to generate path. If none supplied then today will be used
|
||||
(Default value = None)
|
||||
|
||||
Returns:
|
||||
Path: path to the repository root
|
||||
"""
|
||||
date = date or utcnow().date()
|
||||
return (
|
||||
self.paths.archive
|
||||
/ "repos"
|
||||
/ date.strftime("%Y")
|
||||
/ date.strftime("%m")
|
||||
/ date.strftime("%d")
|
||||
/ self.repository_id.name
|
||||
/ self.repository_id.architecture
|
||||
)
|
||||
|
||||
def symlinks_create(self, packages: list[Package]) -> None:
|
||||
"""
|
||||
create symlinks for the specified packages in today's repository
|
||||
|
||||
Args:
|
||||
packages(list[Package]): list of packages to be updated
|
||||
"""
|
||||
root = self.repository_for()
|
||||
repo = self._repo(root)
|
||||
|
||||
for package in packages:
|
||||
archive = self.paths.archive_for(package.base)
|
||||
|
||||
for package_name, single in package.packages.items():
|
||||
if single.filename is None:
|
||||
self.logger.warning("received empty package filename for %s", package_name)
|
||||
continue
|
||||
|
||||
if self._package_symlinks_create(single, root, archive):
|
||||
repo.add(root / single.filename)
|
||||
|
||||
def symlinks_fix(self) -> Iterator[Path]:
|
||||
"""
|
||||
remove broken symlinks across repositories for all dates
|
||||
|
||||
Yields:
|
||||
Path: path of the sub-repository with removed symlinks
|
||||
"""
|
||||
for path in walk(self.paths.archive / "repos"):
|
||||
root = path.parent
|
||||
*_, name, architecture = root.parts
|
||||
if self.repository_id.name != name or self.repository_id.architecture != architecture:
|
||||
continue # we only process same name repositories
|
||||
|
||||
if not package_like(path):
|
||||
continue
|
||||
if not path.is_symlink():
|
||||
continue # find symlinks only
|
||||
if path.exists():
|
||||
continue # filter out not broken symlinks
|
||||
|
||||
# here we don't have access to original archive, so we have to guess name based on archive name
|
||||
# normally it should be fine to do so
|
||||
package_name = path.name.rsplit("-", maxsplit=3)[0]
|
||||
self._repo(root).remove(package_name, path)
|
||||
yield path.parent.relative_to(self.paths.archive / "repos")
|
||||
|
||||
def tree_create(self) -> None:
|
||||
"""
|
||||
create repository tree for current repository
|
||||
"""
|
||||
root = self.repository_for()
|
||||
if root.exists():
|
||||
return
|
||||
|
||||
with self.paths.preserve_owner():
|
||||
root.mkdir(0o755, parents=True)
|
||||
# init empty repository here
|
||||
self._repo(root).init()
|
||||
@@ -1,70 +0,0 @@
|
||||
#
|
||||
# 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 ahriman.core.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.configuration import Configuration
|
||||
from ahriman.core.sign.gpg import GPG
|
||||
from ahriman.core.triggers import Trigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.repository_id import RepositoryId
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
class ArchiveTrigger(Trigger):
|
||||
"""
|
||||
archive repository extension
|
||||
|
||||
Attributes:
|
||||
paths(RepositoryPaths): repository paths instance
|
||||
tree(ArchiveTree): archive tree wrapper
|
||||
"""
|
||||
|
||||
def __init__(self, repository_id: RepositoryId, configuration: Configuration) -> None:
|
||||
"""
|
||||
Args:
|
||||
repository_id(RepositoryId): repository unique identifier
|
||||
configuration(Configuration): configuration instance
|
||||
"""
|
||||
Trigger.__init__(self, repository_id, configuration)
|
||||
|
||||
self.paths = configuration.repository_paths
|
||||
self.tree = ArchiveTree(self.paths, GPG(configuration).repository_sign_args)
|
||||
|
||||
def on_result(self, result: Result, packages: list[Package]) -> None:
|
||||
"""
|
||||
run trigger
|
||||
|
||||
Args:
|
||||
result(Result): build result
|
||||
packages(list[Package]): list of all available packages
|
||||
"""
|
||||
self.tree.symlinks_create(packages)
|
||||
|
||||
def on_start(self) -> None:
|
||||
"""
|
||||
trigger action which will be called at the start of the application
|
||||
"""
|
||||
self.tree.tree_create()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""
|
||||
trigger action which will be called before the stop of the application
|
||||
"""
|
||||
repositories = set(self.tree.symlinks_fix())
|
||||
self.tree.directories_fix(repositories)
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
@@ -1,34 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from ahriman.core.archive import ArchiveTrigger
|
||||
from ahriman.core.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.configuration import Configuration
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archive_tree(configuration: Configuration) -> ArchiveTree:
|
||||
"""
|
||||
archive tree fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
ArchiveTree: archive tree test instance
|
||||
"""
|
||||
return ArchiveTree(configuration.repository_paths, [])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def archive_trigger(configuration: Configuration) -> ArchiveTrigger:
|
||||
"""
|
||||
archive trigger fixture
|
||||
|
||||
Args:
|
||||
configuration(Configuration): configuration fixture
|
||||
|
||||
Returns:
|
||||
ArchiveTrigger: archive trigger test instance
|
||||
"""
|
||||
_, repository_id = configuration.check_loaded()
|
||||
return ArchiveTrigger(repository_id, configuration)
|
||||
@@ -1,176 +0,0 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
from unittest.mock import call as MockCall
|
||||
|
||||
from ahriman.core.archive.archive_tree import ArchiveTree
|
||||
from ahriman.core.utils import utcnow
|
||||
from ahriman.models.package import Package
|
||||
|
||||
|
||||
def test_repo(archive_tree: ArchiveTree) -> None:
|
||||
"""
|
||||
must return correct repository object
|
||||
"""
|
||||
local = Path("local")
|
||||
repo = archive_tree._repo(local)
|
||||
|
||||
assert repo.sign_args == archive_tree.sign_args
|
||||
assert repo.name == archive_tree.repository_id.name
|
||||
assert repo.root == local
|
||||
|
||||
|
||||
def test_repository_for(archive_tree: ArchiveTree) -> None:
|
||||
"""
|
||||
must correctly generate path to repository
|
||||
"""
|
||||
path = archive_tree.repository_for()
|
||||
assert path.is_relative_to(archive_tree.paths.archive / "repos")
|
||||
assert (archive_tree.repository_id.name, archive_tree.repository_id.architecture) == path.parts[-2:]
|
||||
assert set(map("{:02d}".format, utcnow().timetuple()[:3])).issubset(path.parts)
|
||||
|
||||
|
||||
def test_directories_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must remove empty directories recursively
|
||||
"""
|
||||
root = archive_tree.paths.archive / "repos"
|
||||
(root / "a" / "b").mkdir(parents=True, exist_ok=True)
|
||||
(root / "a" / "b" / "file").touch()
|
||||
(root / "a" / "b" / "c" / "d").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_original_rmdir = Path.rmdir
|
||||
rmdir_mock = mocker.patch("pathlib.Path.rmdir", autospec=True, side_effect=_original_rmdir)
|
||||
|
||||
archive_tree.directories_fix({Path("a") / "b" / "c" / "d"})
|
||||
rmdir_mock.assert_has_calls([
|
||||
MockCall(root / "a" / "b" / "c" / "d"),
|
||||
MockCall(root / "a" / "b" / "c"),
|
||||
])
|
||||
|
||||
|
||||
def test_symlinks_create(archive_tree: ArchiveTree, package_ahriman: Package, package_python_schedule: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create symlinks
|
||||
"""
|
||||
_original_exists = Path.exists
|
||||
|
||||
symlinks_mock = mocker.patch("pathlib.Path.symlink_to", side_effect=(None, FileExistsError, FileExistsError))
|
||||
add_mock = mocker.patch("ahriman.core.alpm.repo.Repo.add")
|
||||
mocker.patch("pathlib.Path.glob", autospec=True, side_effect=lambda path, name: [path / name[:-1]])
|
||||
|
||||
archive_tree.symlinks_create([package_ahriman, package_python_schedule])
|
||||
symlinks_mock.assert_has_calls([
|
||||
MockCall(Path("..") /
|
||||
".." /
|
||||
".." /
|
||||
".." /
|
||||
".." /
|
||||
".." /
|
||||
archive_tree.paths.archive_for(package.base)
|
||||
.relative_to(archive_tree.paths.root)
|
||||
.relative_to("archive") /
|
||||
single.filename
|
||||
)
|
||||
for package in (package_ahriman, package_python_schedule)
|
||||
for single in package.packages.values()
|
||||
])
|
||||
add_mock.assert_called_once_with(
|
||||
archive_tree.repository_for() / package_ahriman.packages[package_ahriman.base].filename
|
||||
)
|
||||
|
||||
|
||||
def test_symlinks_create_empty_filename(archive_tree: ArchiveTree, package_ahriman: Package,
|
||||
mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip symlinks creation if filename is not set
|
||||
"""
|
||||
package_ahriman.packages[package_ahriman.base].filename = None
|
||||
symlinks_mock = mocker.patch("pathlib.Path.symlink_to")
|
||||
|
||||
archive_tree.symlinks_create([package_ahriman])
|
||||
symlinks_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_symlinks_fix(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fix broken symlinks
|
||||
"""
|
||||
_original_exists = Path.exists
|
||||
|
||||
def exists_mock(path: Path) -> bool:
|
||||
if path.name.startswith("symlink"):
|
||||
return True
|
||||
return _original_exists(path)
|
||||
|
||||
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
|
||||
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
|
||||
walk_mock = mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
|
||||
archive_tree.repository_for() / filename
|
||||
for filename in (
|
||||
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"symlink-1.0.0-1-x86_64.pkg.tar.zst.sig",
|
||||
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"file-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
)
|
||||
])
|
||||
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
|
||||
|
||||
assert list(archive_tree.symlinks_fix()) == [
|
||||
archive_tree.repository_for().relative_to(archive_tree.paths.archive / "repos"),
|
||||
]
|
||||
walk_mock.assert_called_once_with(archive_tree.paths.archive / "repos")
|
||||
remove_mock.assert_called_once_with(
|
||||
"broken_symlink", archive_tree.repository_for() / "broken_symlink-1.0.0-1-x86_64.pkg.tar.zst")
|
||||
|
||||
|
||||
def test_symlinks_fix_foreign_repository(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip symlinks check if repository name or architecture doesn't match
|
||||
"""
|
||||
_original_exists = Path.exists
|
||||
|
||||
def exists_mock(path: Path) -> bool:
|
||||
if path.name.startswith("symlink"):
|
||||
return True
|
||||
return _original_exists(path)
|
||||
|
||||
mocker.patch("pathlib.Path.is_symlink", side_effect=[True, True, False])
|
||||
mocker.patch("pathlib.Path.exists", autospec=True, side_effect=exists_mock)
|
||||
mocker.patch("ahriman.core.archive.archive_tree.walk", return_value=[
|
||||
archive_tree.repository_for().with_name("i686") / filename
|
||||
for filename in (
|
||||
"symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"broken_symlink-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
"file-1.0.0-1-x86_64.pkg.tar.zst",
|
||||
)
|
||||
])
|
||||
remove_mock = mocker.patch("ahriman.core.alpm.repo.Repo.remove")
|
||||
|
||||
assert list(archive_tree.symlinks_fix()) == []
|
||||
remove_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_tree_create(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create repository root if not exists
|
||||
"""
|
||||
owner_guard_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.preserve_owner")
|
||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||
init_mock = mocker.patch("ahriman.core.alpm.repo.Repo.init")
|
||||
|
||||
archive_tree.tree_create()
|
||||
owner_guard_mock.assert_called_once_with()
|
||||
mkdir_mock.assert_called_once_with(0o755, parents=True)
|
||||
init_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_tree_create_exists(archive_tree: ArchiveTree, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must skip directory creation if already exists
|
||||
"""
|
||||
mocker.patch("pathlib.Path.exists", return_value=True)
|
||||
mkdir_mock = mocker.patch("pathlib.Path.mkdir")
|
||||
|
||||
archive_tree.tree_create()
|
||||
mkdir_mock.assert_not_called()
|
||||
@@ -1,37 +0,0 @@
|
||||
from pathlib import Path
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from ahriman.core.archive import ArchiveTrigger
|
||||
from ahriman.models.package import Package
|
||||
from ahriman.models.result import Result
|
||||
|
||||
|
||||
def test_on_result(archive_trigger: ArchiveTrigger, package_ahriman: Package, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create symlinks for actual repository
|
||||
"""
|
||||
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_create")
|
||||
archive_trigger.on_result(Result(), [package_ahriman])
|
||||
symlinks_mock.assert_called_once_with([package_ahriman])
|
||||
|
||||
|
||||
def test_on_start(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must create repository tree on load
|
||||
"""
|
||||
tree_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.tree_create")
|
||||
archive_trigger.on_start()
|
||||
tree_mock.assert_called_once_with()
|
||||
|
||||
|
||||
def test_on_stop(archive_trigger: ArchiveTrigger, mocker: MockerFixture) -> None:
|
||||
"""
|
||||
must fix broken symlinks on stop
|
||||
"""
|
||||
local = Path("local")
|
||||
symlinks_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.symlinks_fix", return_value=[local])
|
||||
directories_mock = mocker.patch("ahriman.core.archive.archive_tree.ArchiveTree.directories_fix")
|
||||
|
||||
archive_trigger.on_stop()
|
||||
symlinks_mock.assert_called_once_with()
|
||||
directories_mock.assert_called_once_with({local})
|
||||
@@ -1,16 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ahriman</title>
|
||||
|
||||
<link rel="icon" href="/static/favicon.ico" />
|
||||
<script type="module" crossorigin src="/static/index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,157 +0,0 @@
|
||||
<div id="dashboard-modal" tabindex="-1" role="dialog" class="modal fade">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div id="dashboard-modal-header" class="modal-header">
|
||||
<h4 class="modal-title">System health</h4>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Repository name</div>
|
||||
<div id="dashboard-name" class="col-8 col-lg-3"></div>
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Repository architecture</div>
|
||||
<div id="dashboard-architecture" class="col-8 col-lg-3"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row mt-2">
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Current status</div>
|
||||
<div id="dashboard-status" class="col-8 col-lg-3"></div>
|
||||
<div class="col-4 col-lg-2" style="text-align: right">Updated at</div>
|
||||
<div id="dashboard-status-timestamp" class="col-8 col-lg-3"></div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard-canvas" class="form-group row mt-2">
|
||||
<div class="col-8 col-lg-6">
|
||||
<canvas id="dashboard-packages-count-chart"></canvas>
|
||||
</div>
|
||||
<div class="col-8 col-lg-6">
|
||||
<canvas id="dashboard-packages-statuses-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const dashboardModal = document.getElementById("dashboard-modal");
|
||||
const dashboardModalHeader = document.getElementById("dashboard-modal-header");
|
||||
|
||||
const dashboardName = document.getElementById("dashboard-name");
|
||||
const dashboardArchitecture = document.getElementById("dashboard-architecture");
|
||||
const dashboardStatus = document.getElementById("dashboard-status");
|
||||
const dashboardStatusTimestamp = document.getElementById("dashboard-status-timestamp");
|
||||
|
||||
const dashboardCanvas = document.getElementById("dashboard-canvas");
|
||||
const dashboardPackagesStatusesChartCanvas = document.getElementById("dashboard-packages-statuses-chart");
|
||||
let dashboardPackagesStatusesChart = null;
|
||||
const dashboardPackagesCountChartCanvas = document.getElementById("dashboard-packages-count-chart");
|
||||
let dashboardPackagesCountChart = null;
|
||||
|
||||
function statusLoad() {
|
||||
const badgeClass = status => {
|
||||
if (status === "pending") return "btn-outline-warning";
|
||||
if (status === "building") return "btn-outline-warning";
|
||||
if (status === "failed") return "btn-outline-danger";
|
||||
if (status === "success") return "btn-outline-success";
|
||||
return "btn-outline-secondary";
|
||||
};
|
||||
|
||||
makeRequest(
|
||||
"/api/v1/status",
|
||||
{
|
||||
query: {
|
||||
architecture: repository.architecture,
|
||||
repository: repository.repository,
|
||||
},
|
||||
convert: response => response.json(),
|
||||
},
|
||||
data => {
|
||||
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
|
||||
|
||||
dashboardButton.classList.remove(...dashboardButton.classList);
|
||||
dashboardButton.classList.add("btn");
|
||||
dashboardButton.classList.add(badgeClass(data.status.status));
|
||||
|
||||
dashboardModalHeader.classList.remove(...dashboardModalHeader.classList);
|
||||
dashboardModalHeader.classList.add("modal-header");
|
||||
headerClass(data.status.status).forEach(clz => dashboardModalHeader.classList.add(clz));
|
||||
|
||||
dashboardName.textContent = data.repository;
|
||||
dashboardArchitecture.textContent = data.architecture;
|
||||
dashboardStatus.textContent = data.status.status;
|
||||
dashboardStatusTimestamp.textContent = new Date(1000 * data.status.timestamp).toISOStringShort();
|
||||
|
||||
if (dashboardPackagesStatusesChart) {
|
||||
const labels = [
|
||||
"unknown",
|
||||
"pending",
|
||||
"building",
|
||||
"failed",
|
||||
"success",
|
||||
];
|
||||
dashboardPackagesStatusesChart.config.data = {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: "packages in status",
|
||||
data: labels.map(label => data.packages[label]),
|
||||
backgroundColor: [
|
||||
"rgb(55, 58, 60)",
|
||||
"rgb(255, 117, 24)",
|
||||
"rgb(255, 117, 24)",
|
||||
"rgb(255, 0, 57)",
|
||||
"rgb(63, 182, 24)", // copy-paste from current style
|
||||
],
|
||||
}],
|
||||
};
|
||||
dashboardPackagesStatusesChart.update();
|
||||
}
|
||||
|
||||
if (dashboardPackagesCountChart) {
|
||||
dashboardPackagesCountChart.config.data = {
|
||||
labels: ["packages"],
|
||||
datasets: [
|
||||
{
|
||||
label: "archives",
|
||||
data: [data.stats.packages],
|
||||
},
|
||||
{
|
||||
label: "bases",
|
||||
data: [data.stats.bases],
|
||||
},
|
||||
],
|
||||
};
|
||||
dashboardPackagesCountChart.update();
|
||||
}
|
||||
|
||||
dashboardCanvas.hidden = data.status.total > 0;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ready(_ => {
|
||||
dashboardPackagesStatusesChart = new Chart(dashboardPackagesStatusesChartCanvas, {
|
||||
type: "pie",
|
||||
data: {},
|
||||
options: {
|
||||
responsive: true,
|
||||
},
|
||||
});
|
||||
dashboardPackagesCountChart = new Chart(dashboardPackagesCountChartCanvas, {
|
||||
type: "bar",
|
||||
data: {},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
stacked: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,247 +0,0 @@
|
||||
<script>
|
||||
const packageRemoveButton = document.getElementById("package-remove-button");
|
||||
const packageUpdateButton = document.getElementById("package-update-button");
|
||||
|
||||
let repository = null;
|
||||
|
||||
// so far bootstrap-table only operates with jquery elements
|
||||
const table = $(document.getElementById("packages"));
|
||||
|
||||
const dashboardButton = document.getElementById("dashboard-button");
|
||||
const versionBadge = document.getElementById("badge-version");
|
||||
|
||||
const tableAutoReloadButton = document.getElementById("table-autoreload-button");
|
||||
const tableAutoReloadInput = document.getElementById("table-autoreload-input");
|
||||
let tableAutoReloadTask = null;
|
||||
|
||||
function doPackageAction(uri, packages, repository, successText, failureText, data) {
|
||||
makeRequest(
|
||||
uri,
|
||||
{
|
||||
method: "POST",
|
||||
query: {
|
||||
architecture: repository.architecture,
|
||||
repository: repository.repository,
|
||||
},
|
||||
json: Object.assign({}, {packages: packages}, data || {}),
|
||||
},
|
||||
_ => {
|
||||
const message = successText(packages.join(", "));
|
||||
showSuccess("Success", message);
|
||||
},
|
||||
error => {
|
||||
showFailure("Action failed", failureText, error);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function filterListGroups() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "groups");
|
||||
}
|
||||
|
||||
function filterListLicenses() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "licenses");
|
||||
}
|
||||
|
||||
function filterListPackagers() {
|
||||
return extractDataList(table.bootstrapTable("getData"), "packager");
|
||||
}
|
||||
|
||||
function getRepositorySelector(selector) {
|
||||
const selected = selector.options[selector.selectedIndex];
|
||||
return {
|
||||
architecture: selected.getAttribute("data-architecture"),
|
||||
repository: selected.getAttribute("data-repository"),
|
||||
};
|
||||
}
|
||||
|
||||
function getSelection() {
|
||||
return table.bootstrapTable("getSelections").map(row => row.id);
|
||||
}
|
||||
|
||||
function packagesLoad(onFailure) {
|
||||
makeRequest(
|
||||
"/api/v1/packages",
|
||||
{
|
||||
query: {
|
||||
architecture: repository.architecture,
|
||||
repository: repository.repository,
|
||||
},
|
||||
convert: response => response.json(),
|
||||
},
|
||||
data => {
|
||||
const payload = data
|
||||
.map(description => {
|
||||
const package_base = description.package.base;
|
||||
const web_url = description.package.remote.web_url;
|
||||
return {
|
||||
id: package_base,
|
||||
base: web_url ? safeLink(web_url, package_base, package_base).outerHTML : safe(package_base),
|
||||
version: safe(description.package.version),
|
||||
packager: description.package.packager ? safe(description.package.packager) : "",
|
||||
packages: listToTable(Object.keys(description.package.packages)),
|
||||
groups: listToTable(extractListProperties(description.package, "groups")),
|
||||
licenses: listToTable(extractListProperties(description.package, "licenses")),
|
||||
timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(),
|
||||
status: description.status.status,
|
||||
};
|
||||
});
|
||||
|
||||
updateTable(table, payload, row => row.timestamp);
|
||||
table.bootstrapTable("hideLoading");
|
||||
},
|
||||
onFailure,
|
||||
);
|
||||
}
|
||||
|
||||
function packagesRemove(packages) {
|
||||
packages = packages ?? getSelection();
|
||||
const onSuccess = update => `Packages ${update} have been removed`;
|
||||
const onFailure = error => `Could not remove packages: ${error}`;
|
||||
|
||||
doPackageAction("/api/v1/service/remove", packages, repository, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
function packagesUpdate() {
|
||||
const currentSelection = getSelection();
|
||||
const [url, onSuccess] = currentSelection.length === 0
|
||||
? ["/api/v1/service/update", _ => `Repository update has been run`]
|
||||
: ["/api/v1/service/add", update => `Run update for packages ${update}`];
|
||||
const onFailure = error => `Packages update failed: ${error}`;
|
||||
|
||||
doPackageAction(url, currentSelection, repository, onSuccess, onFailure);
|
||||
}
|
||||
|
||||
function refreshDatabases() {
|
||||
const onSuccess = _ => "Pacman database update has been requested";
|
||||
const onFailure = error => `Could not update pacman databases: ${error}`;
|
||||
const parameters = {
|
||||
refresh: true,
|
||||
aur: false,
|
||||
local: false,
|
||||
manual: false,
|
||||
};
|
||||
|
||||
doPackageAction("/api/v1/service/update", [], repository, onSuccess, onFailure, parameters);
|
||||
}
|
||||
|
||||
function reload() {
|
||||
table.bootstrapTable("showLoading");
|
||||
const onFailure = error => {
|
||||
if ((error.status === 401) || (error.status === 403)) {
|
||||
// authorization error
|
||||
const text = "In order to see statuses you must login first.";
|
||||
table.find("tr.unauthorized").remove();
|
||||
table.find("tbody").append(`<tr class="unauthorized"><td colspan="100%">${safe(text)}</td></tr>`);
|
||||
table.bootstrapTable("hideLoading");
|
||||
} else {
|
||||
// other errors
|
||||
const message = details => `Could not load list of packages: ${details}`;
|
||||
showFailure("Load failure", message, error);
|
||||
}
|
||||
};
|
||||
|
||||
packagesLoad(onFailure);
|
||||
statusLoad();
|
||||
}
|
||||
|
||||
function selectRepository() {
|
||||
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
|
||||
document.getElementById(`${fragment}-link`).click();
|
||||
}
|
||||
|
||||
function statusFormat(value) {
|
||||
const cellClass = status => {
|
||||
if (status === "pending") return "table-warning";
|
||||
if (status === "building") return "table-warning";
|
||||
if (status === "failed") return "table-danger";
|
||||
if (status === "success") return "table-success";
|
||||
return "table-secondary";
|
||||
};
|
||||
return {classes: cellClass(value)};
|
||||
}
|
||||
|
||||
function toggleTableAutoReload(interval) {
|
||||
clearInterval(tableAutoReloadTask);
|
||||
tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => {
|
||||
if (!hasActiveModal() &&
|
||||
!hasActiveDropdown()) {
|
||||
packagesLoad();
|
||||
statusLoad();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ready(_ => {
|
||||
const onCheckFunction = function () {
|
||||
if (packageRemoveButton) {
|
||||
packageRemoveButton.disabled = !getSelection().length;
|
||||
}
|
||||
};
|
||||
|
||||
document.querySelectorAll("#repositories a").forEach(element => {
|
||||
element.onclick = _ => {
|
||||
repository = {
|
||||
architecture: element.dataset.architecture,
|
||||
repository: element.dataset.repository,
|
||||
};
|
||||
if (packageUpdateButton) {
|
||||
packageUpdateButton.innerHTML = `<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`;
|
||||
}
|
||||
bootstrap.Tab.getOrCreateInstance(document.getElementById(element.id)).show();
|
||||
reload();
|
||||
};
|
||||
});
|
||||
|
||||
table.bootstrapTable({
|
||||
onCheck: onCheckFunction,
|
||||
onCheckAll: onCheckFunction,
|
||||
onClickRow: (data, row, cell) => {
|
||||
if (0 === cell || "base" === cell) {
|
||||
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
|
||||
table.bootstrapTable(method, {field: "id", values: [data.id]});
|
||||
} else showPackageInfo(data.id);
|
||||
},
|
||||
onCreatedControls: _ => {
|
||||
new easepick.create({
|
||||
element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
|
||||
css: [
|
||||
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
|
||||
],
|
||||
grid: 2,
|
||||
calendars: 2,
|
||||
autoApply: false,
|
||||
locale: {
|
||||
cancel: "Clear",
|
||||
},
|
||||
RangePlugin: {
|
||||
tooltip: false,
|
||||
},
|
||||
plugins: [
|
||||
"RangePlugin",
|
||||
],
|
||||
setup: picker => {
|
||||
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
|
||||
// replace "Cancel" behaviour to "Clear"
|
||||
picker.onClickCancelButton = element => {
|
||||
if (picker.isCancelButton(element)) {
|
||||
picker.clear();
|
||||
picker.hide();
|
||||
table.bootstrapTable("triggerSearch");
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
},
|
||||
onUncheck: onCheckFunction,
|
||||
onUncheckAll: onCheckFunction,
|
||||
});
|
||||
|
||||
restoreAutoReloadSettings(tableAutoReloadButton, tableAutoReloadInput);
|
||||
|
||||
selectRepository();
|
||||
{% if autorefresh_intervals %}
|
||||
toggleTableAutoReload();
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
@@ -1,55 +0,0 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "ahriman-web"
|
||||
description = "ArcH linux ReposItory MANager, web server"
|
||||
readme = "../README.md"
|
||||
requires-python = ">=3.13"
|
||||
license = {file = "../COPYING"}
|
||||
authors = [
|
||||
{name = "ahriman team"},
|
||||
]
|
||||
dependencies = [
|
||||
"ahriman-core",
|
||||
"aiohttp",
|
||||
"aiohttp_cors",
|
||||
"aiohttp_jinja2",
|
||||
"aiohttp_sse",
|
||||
]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
auth = [
|
||||
"aiohttp_session",
|
||||
"aiohttp_security",
|
||||
"cryptography",
|
||||
]
|
||||
docs = [
|
||||
"aiohttp-apispec",
|
||||
"setuptools",
|
||||
]
|
||||
metrics = [
|
||||
"aiohttp-openmetrics",
|
||||
]
|
||||
oauth2 = [
|
||||
"ahriman-web[auth]",
|
||||
"aioauth-client",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Documentation = "https://ahriman.readthedocs.io/"
|
||||
Repository = "https://github.com/arcan1s/ahriman"
|
||||
Changelog = "https://github.com/arcan1s/ahriman/releases"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "../ahriman-core/src/ahriman/__init__.py"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
only-include = ["src/ahriman"]
|
||||
sources = ["src"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.shared-data]
|
||||
"package/lib" = "lib"
|
||||
"package/share" = "share"
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
@@ -1,19 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
@@ -1,61 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import hashlib
|
||||
|
||||
from aiohttp import ETag
|
||||
from aiohttp.typedefs import Middleware
|
||||
from aiohttp.web import HTTPNotModified, Request, Response, StreamResponse, middleware
|
||||
|
||||
from ahriman.web.middlewares import HandlerType
|
||||
|
||||
|
||||
__all__ = ["etag_handler"]
|
||||
|
||||
|
||||
def etag_handler() -> Middleware:
|
||||
"""
|
||||
middleware to handle ETag header for conditional requests. It computes ETag from the response body
|
||||
and returns 304 Not Modified if the client sends a matching ``If-None-Match`` header
|
||||
|
||||
Returns:
|
||||
Middleware: built middleware
|
||||
|
||||
Raises:
|
||||
HTTPNotModified: if content matches ``If-None-Match`` header sent
|
||||
"""
|
||||
@middleware
|
||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||
response = await handler(request)
|
||||
|
||||
if not isinstance(response, Response) or not isinstance(response.body, bytes):
|
||||
return response
|
||||
|
||||
if request.method not in ("GET", "HEAD"):
|
||||
return response
|
||||
|
||||
etag = ETag(value=hashlib.md5(response.body, usedforsecurity=False).hexdigest())
|
||||
response.etag = etag
|
||||
|
||||
if request.if_none_match is not None and etag in request.if_none_match:
|
||||
raise HTTPNotModified(headers={"ETag": response.headers["ETag"]})
|
||||
|
||||
return response
|
||||
|
||||
return handle
|
||||
@@ -1,69 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
try:
|
||||
import aiohttp_openmetrics
|
||||
except ImportError:
|
||||
aiohttp_openmetrics = None # type: ignore[assignment]
|
||||
|
||||
from aiohttp.typedefs import Middleware
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse, middleware
|
||||
|
||||
from ahriman.web.middlewares import HandlerType
|
||||
|
||||
|
||||
__all__ = [
|
||||
"metrics",
|
||||
"metrics_handler",
|
||||
]
|
||||
|
||||
|
||||
async def metrics(request: Request) -> Response:
|
||||
"""
|
||||
handler for returning metrics
|
||||
|
||||
Args:
|
||||
request(Request): request object
|
||||
|
||||
Returns:
|
||||
Response: response object
|
||||
|
||||
Raises:
|
||||
HTTPNotFound: endpoint is disabled
|
||||
"""
|
||||
if aiohttp_openmetrics is None:
|
||||
raise HTTPNotFound
|
||||
return await aiohttp_openmetrics.metrics(request)
|
||||
|
||||
|
||||
def metrics_handler() -> Middleware:
|
||||
"""
|
||||
middleware for metrics support
|
||||
|
||||
Returns:
|
||||
Middleware: middleware function to handle server metrics
|
||||
"""
|
||||
if aiohttp_openmetrics is not None:
|
||||
return aiohttp_openmetrics.metrics_middleware
|
||||
|
||||
@middleware
|
||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||
return await handler(request)
|
||||
|
||||
return handle
|
||||
@@ -1,51 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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/>.
|
||||
#
|
||||
import uuid
|
||||
|
||||
from aiohttp.typedefs import Middleware
|
||||
from aiohttp.web import Request, StreamResponse, middleware
|
||||
|
||||
from ahriman.core.log.log_context import LogContext
|
||||
from ahriman.web.middlewares import HandlerType
|
||||
|
||||
|
||||
__all__ = ["request_id_handler"]
|
||||
|
||||
|
||||
def request_id_handler() -> Middleware:
|
||||
"""
|
||||
middleware to trace request id header
|
||||
|
||||
Returns:
|
||||
Middleware: request id processing middleware
|
||||
"""
|
||||
@middleware
|
||||
async def handle(request: Request, handler: HandlerType) -> StreamResponse:
|
||||
request_id = request.headers.getone("X-Request-ID", str(uuid.uuid4()))
|
||||
|
||||
token = LogContext.set("request_id", request_id)
|
||||
try:
|
||||
response = await handler(request)
|
||||
response.headers["X-Request-ID"] = request_id
|
||||
return response
|
||||
finally:
|
||||
LogContext.reset("request_id", token)
|
||||
|
||||
return handle
|
||||
@@ -1,26 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.web.apispec import Schema
|
||||
|
||||
|
||||
class AnySchema(Schema):
|
||||
"""
|
||||
response dummy schema
|
||||
"""
|
||||
@@ -1,41 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class AuthInfoSchema(Schema):
|
||||
"""
|
||||
authorization information schema
|
||||
"""
|
||||
|
||||
control = fields.String(
|
||||
metadata={
|
||||
"description": "HTML control for login interface",
|
||||
"example": "<button type=\"button\" class=\"btn btn-link\" data-bs-toggle=\"modal\" data-bs-target=\"#login-modal\" style=\"text-decoration: none\"><i class=\"bi bi-box-arrow-in-right\"></i> login</button>",
|
||||
})
|
||||
enabled = fields.Boolean(required=True, metadata={
|
||||
"description": "Whether authentication is enabled or not",
|
||||
})
|
||||
external = fields.Boolean(required=True, metadata={
|
||||
"description": "Whether authorization provider is external (e.g. OAuth)",
|
||||
})
|
||||
username = fields.String(metadata={
|
||||
"description": "Currently authenticated username if available",
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class AutoRefreshIntervalSchema(Schema):
|
||||
"""
|
||||
auto refresh interval schema
|
||||
"""
|
||||
|
||||
interval = fields.Integer(required=True, metadata={
|
||||
"description": "Auto refresh interval in milliseconds",
|
||||
"example": "60000",
|
||||
})
|
||||
is_active = fields.Boolean(required=True, metadata={
|
||||
"description": "Whether this interval is the default active one",
|
||||
})
|
||||
text = fields.String(required=True, metadata={
|
||||
"description": "Human readable interval description",
|
||||
"example": "1 minute",
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class ConfigurationSchema(Schema):
|
||||
"""
|
||||
response configuration schema
|
||||
"""
|
||||
|
||||
key = fields.String(required=True, metadata={
|
||||
"description": "Configuration key",
|
||||
"example": "host",
|
||||
})
|
||||
section = fields.String(required=True, metadata={
|
||||
"description": "Configuration section",
|
||||
"example": "web",
|
||||
})
|
||||
value = fields.String(required=True, metadata={
|
||||
"description": "Configuration value",
|
||||
"example": "127.0.0.1",
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman.web.apispec import Schema, fields
|
||||
|
||||
|
||||
class HoldSchema(Schema):
|
||||
"""
|
||||
request hold schema
|
||||
"""
|
||||
|
||||
is_held = fields.Boolean(required=True, metadata={
|
||||
"description": "Package hold status",
|
||||
})
|
||||
@@ -1,51 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2021-2026 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 ahriman import __version__
|
||||
from ahriman.web.apispec import Schema, fields
|
||||
from ahriman.web.schemas.auth_info_schema import AuthInfoSchema
|
||||
from ahriman.web.schemas.auto_refresh_interval_schema import AutoRefreshIntervalSchema
|
||||
from ahriman.web.schemas.repository_id_schema import RepositoryIdSchema
|
||||
|
||||
|
||||
class InfoV2Schema(Schema):
|
||||
"""
|
||||
response service information schema
|
||||
"""
|
||||
|
||||
auth = fields.Nested(AuthInfoSchema(), required=True, metadata={
|
||||
"description": "Authorization descriptor",
|
||||
})
|
||||
autorefresh_intervals = fields.Nested(AutoRefreshIntervalSchema(many=True), metadata={
|
||||
"description": "Available auto refresh intervals",
|
||||
})
|
||||
docs_enabled = fields.Boolean(metadata={
|
||||
"description": "Whether API documentation is enabled",
|
||||
})
|
||||
index_url = fields.String(metadata={
|
||||
"description": "URL to the repository index page",
|
||||
"example": "https://ahriman.readthedocs.io/",
|
||||
})
|
||||
repositories = fields.Nested(RepositoryIdSchema(many=True), required=True, metadata={
|
||||
"description": "List of loaded repositories",
|
||||
})
|
||||
version = fields.String(required=True, metadata={
|
||||
"description": "Service version",
|
||||
"example": __version__,
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user