Compare commits

...

20 Commits

Author SHA1 Message Date
969352d842 Release 2.11.0 2023-08-27 02:12:21 +03:00
a1db4dc8b8 add ability to partition tree before calculationn 2023-08-27 01:55:57 +03:00
f6081507c0 small improvements on code smell
* fix some grammar/typo errors
* change some statements to be more clear
* use pattern matching for enum processing
2023-08-25 04:15:10 +03:00
477c473187 force rtd theme
Since Aug, 22 rtd doesn't force its theme anymore, leaving it as default
2023-08-23 15:27:51 +03:00
33e68a59e2 use http client class for all http requests 2023-08-23 03:27:42 +03:00
6dfe1b92f2 bump pylintrc 2023-08-21 02:55:08 +03:00
5dc6df11c5 verbose subprocess exception handle annd quite git 2023-08-21 01:35:43 +03:00
d3f6ca24c8 review exception raise
In some cases for better readability of logs, exceptions are now raised
without parent exception stacktrace. Also updated docs and contributing
guidelines
2023-08-20 17:03:46 +03:00
c26a13c562 Remote call trigger support (#105)
* add support of remote task tracking
* add remote call trigger implementation
* docs update
* add cross-service upload
* add notes about user
* add more ability to control upload
* multipart upload with signatures as well as safe file save
* configuration reference update
* rename watcher methods
* erase logs based on current package version

Old implementation has used process id instead, but it leads to log
removal in case of remote process trigger

* add --server flag for setup command
* restore behavior of the httploghandler
2023-08-20 03:44:31 +03:00
9ea3a911f7 update gh actions install script 2023-08-17 16:33:02 +03:00
ca60317750 add dummy type fields for pkgbuild generators
This field is required in order to pass config validation in case if
section name differs from default one. Also by default keyring_generator
and mirrorlist_generator have been renamed to keyring-generator and
mirrorlist-generator respectively for consistence
2023-08-17 16:02:15 +03:00
1384efb31d close descriptor after uploading archive on github 2023-08-15 02:31:46 +03:00
8c6486c233 contributing guide update 2023-08-14 02:51:14 +03:00
a1d0e993a8 resoolve dependencies by using local cache too (#107) 2023-08-14 02:31:24 +03:00
572880eb73 add ability to read values from environment variables
It makes sense to read some values from environment. In particular this
feature is useful in case of running application in containers in ci/cd

See #108 for more details
2023-08-14 01:48:08 +03:00
d9eaf17a11 remove unused absolute path validator (#106)
Extracted path is always absolute, so there is no need to check it
2023-08-13 20:48:07 +03:00
95e29d16bb Local packages support improvements (#104)
* handle git author correctly
* make remote source required argument
2023-08-13 15:45:53 +03:00
1f2d56e605 make auth.salt parameter optional
Used implementation of the hasher includes salt itself, thus additional
salt is optional and can be safely (in terms of security) treat as empty
string
2023-08-11 16:31:47 +03:00
1baf04998d full support of pep517
Since llast upgrade build is broken. Lets fully migrate to
pyproject.toml. Note for maintaners: because data_files option is
deprectated (see https://github.com/pypa/setuptools/discussions/2648)
you will have to install files manually inside your packaging process
2023-08-11 03:55:31 +03:00
3a88d00db0 automatically bump pkgrel on version duplicates
The new --(no-)increment flag has been added to add, update and rebuild
subcommands. In case if it is true and package version is the same as in
repository, it will automatically bump pkgrel appending (increasing)
minor part of it (e.g. 1.0.0-1 -> 1.0.0-1.1).

Inn order to implement this, the shadow (e.g. it will not store it in
database) patch for pkgrel will be created
2023-08-08 03:14:47 +03:00
238 changed files with 10436 additions and 6999 deletions

View File

@ -12,7 +12,7 @@ pacman --noconfirm -Syu
# main dependencies
pacman --noconfirm -Sy base-devel devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo python-systemd sudo
# make dependencies
pacman --noconfirm -Sy python-build python-installer python-wheel
pacman --noconfirm -Sy python-build python-flit python-installer python-wheel
# optional dependencies
if [[ -z $MINIMAL_INSTALL ]]; then
# VCS support
@ -34,8 +34,6 @@ pacman --noconfirm -U ahriman-1.0.0-1-any.pkg.tar.zst
# create machine-id which is required by build tools
systemd-machine-id-setup
# special thing for the container, because /dev/log interface is not available there
sed -i "s/handlers = syslog_handler/handlers = console_handler/g" /etc/ahriman.ini.d/logging.ini
# initial setup command as root
[[ -z $MINIMAL_INSTALL ]] && WEB_ARGS=("--web-port" "8080")
ahriman -a x86_64 service-setup --packager "ahriman bot <ahriman@example.com>" --repository "github" "${WEB_ARGS[@]}"
@ -48,10 +46,8 @@ if [[ -z $MINIMAL_INSTALL ]]; then
# run web service (detached)
sudo -u ahriman -- ahriman -a x86_64 web &
WEB_PID=$!
sleep 15s # wait for the web service activation
fi
# add the first package
# the build itself does not really work in the container
sudo -u ahriman -- ahriman package-add --now yay
# check if package was actually installed
test -n "$(find "/var/lib/ahriman/repository/x86_64" -name "yay*pkg*")"

804
.pylintrc
View File

@ -1,28 +1,78 @@
[MASTER]
[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=
# Specify a score threshold to be exceeded before program exits with error.
fail-under=10.0
# 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=
# Add files or directories to the blacklist. They should be base names, not
# paths.
# 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 regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# 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=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=0
# 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
@ -36,6 +86,19 @@ load-plugins=
# 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
@ -44,120 +107,8 @@ suggestion-mode=yes
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# 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 reenable 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,
too-many-ancestors,
fixme,
too-many-arguments,
duplicate-code,
cyclic-import,
confusing-with-statement,
# 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
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'error', 'warning', 'refactor', and 'convention'
# 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=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=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[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
[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=400
# 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
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
@ -166,13 +117,15 @@ single-line-if-stmt=no
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
# 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=
@ -192,20 +145,30 @@ bad-names-rgxs=
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
# 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.
# 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=
@ -217,7 +180,8 @@ docstring-min-length=-1
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
# 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.
@ -239,21 +203,22 @@ include-naming-hint=no
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
# 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.
# 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.
# 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
@ -269,209 +234,56 @@ no-docstring-rgx=^_
# 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.
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[TYPECHECK]
[CLASSES]
# 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
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# 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=
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
asyncSetUp,
__post_init__
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# 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 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
# 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=
# 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
# List of decorators that change the signature of a decorated function.
signature-mutators=
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[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
[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=
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the python-enchant package.
spelling-dict=
# 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
[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 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. Default to name
# with leading underscore.
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
[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
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# 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
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled).
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled).
import-graph=
# Create a graph of internal dependencies in 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=
# 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
@ -503,35 +315,331 @@ max-statements=50
min-public-methods=2
[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,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# 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=cls
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=builtins.BaseException,
builtins.Exception
# 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,
# 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

View File

@ -21,4 +21,3 @@ python:
- docs
- s3
- web
system_packages: true

View File

@ -46,7 +46,7 @@ Again, the most checks can be performed by `make check` command, though some add
int: result
Raises:
RuntimeException: a local function error occurs
RuntimeError: a local function error occurs
Examples:
Very informative example how to use this function, e.g.::
@ -85,7 +85,7 @@ Again, the most checks can be performed by `make check` command, though some add
Args:
*args(Any): positional arguments
**kwargs(Any): keyword arguments
**kwargs(Any): keyword arguments
"""
self.instance_attribute = ""
```
@ -130,6 +130,12 @@ Again, the most checks can be performed by `make check` command, though some add
* Configuration interactions must go through `ahriman.core.configuration.Configuration` class instance.
* In case if class load requires some actions, it is recommended to create class method which can be used for class instantiating.
* The code must follow the exception safety, unless it is explicitly asked by end user. It means that most exceptions must be handled and printed to log, no other actions must be done (e.g. raising another exception).
* Exceptions without parameters should be raised without parentheses, e.g.:
```python
raise RuntimeError
```
* For the external command `ahriman.core.util.check_output` function must be used.
* Every temporary file/directory must be removed at the end of processing, no matter what. The `tempfile` module provides good ways to do it.
* Import order must be the following:
@ -158,7 +164,7 @@ Again, the most checks can be performed by `make check` command, though some add
* 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.version` module. `ahriman.core.context` is also special case.
* 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.
* If your class writes anything to log, the `ahriman.core.log.LazyLogging` trait must be used.
* Web API methods must be documented by using `aiohttp_apispec` library. Schema testing mostly should be implemented in related view class tests. Recommended example for documentation (excluding comments):

View File

@ -11,6 +11,7 @@ ENV AHRIMAN_PACKAGER="ahriman bot <ahriman@example.com>"
ENV AHRIMAN_PACMAN_MIRROR=""
ENV AHRIMAN_PORT=""
ENV AHRIMAN_REPOSITORY="aur-clone"
ENV AHRIMAN_REPOSITORY_SERVER=""
ENV AHRIMAN_REPOSITORY_ROOT="/var/lib/ahriman/ahriman"
ENV AHRIMAN_UNIX_SOCKET=""
ENV AHRIMAN_USER="ahriman"
@ -29,7 +30,7 @@ COPY "docker/install-aur-package.sh" "/usr/local/bin/install-aur-package"
## install package dependencies
## darcs is not installed by reasons, because it requires a lot haskell packages which dramatically increase image size
RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-inflection python-passlib python-requests python-srcinfo && \
pacman -Sy --noconfirm --asdeps python-build python-installer python-wheel && \
pacman -Sy --noconfirm --asdeps python-build python-flit python-installer python-wheel && \
pacman -Sy --noconfirm --asdeps breezy mercurial python-aiohttp python-aiohttp-cors python-boto3 python-cryptography python-jinja python-requests-unixsocket python-systemd rsync subversion && \
runuser -u build -- install-aur-package python-aioauth-client python-aiohttp-apispec-git python-aiohttp-jinja2 \
python-aiohttp-debugtoolbar python-aiohttp-session python-aiohttp-security
@ -39,7 +40,7 @@ RUN pacman -Sy --noconfirm --asdeps devtools git pyalpm python-cerberus python-i
COPY --chown=build . "/home/build/ahriman"
## create package archive and install it
RUN cd "/home/build/ahriman" && \
make VERSION=$(python -c "from src.ahriman.version import __version__; print(__version__)") archlinux && \
make VERSION=$(python -c "from src.ahriman import __version__; print(__version__)") archlinux && \
cp ./*-src.tar.xz "package/archlinux" && \
cd "package/archlinux" && \
runuser -u build -- makepkg --noconfirm --install --skipchecksums && \

View File

@ -3,7 +3,7 @@
PROJECT := ahriman
FILES := AUTHORS CONTRIBUTING.md COPYING Makefile README.md SECURITY.md docs package src setup.py tox.ini web.png
FILES := AUTHORS CONTRIBUTING.md COPYING Makefile README.md SECURITY.md docs package pyproject.toml src tox.ini web.png
TARGET_FILES := $(addprefix $(PROJECT)/, $(FILES))
IGNORE_FILES := package/archlinux src/.mypy_cache
@ -38,7 +38,7 @@ html: specification
tox -e docs-html
push: specification archlinux
git add package/archlinux/PKGBUILD src/ahriman/version.py docs/ahriman-architecture.svg docs/ahriman.1 docs/completions/
git add package/archlinux/PKGBUILD src/ahriman/__init__.py docs/ahriman-architecture.svg package/share/man/man1/ahriman.1 package/share/bash-completion/completions/_ahriman package/share/zsh/site-functions/_ahriman
git commit -m "Release $(VERSION)"
git tag "$(VERSION)"
git push
@ -56,4 +56,4 @@ version:
ifndef VERSION
$(error VERSION is required, but not set)
endif
sed -i 's/^__version__ = .*/__version__ = "$(VERSION)"/' src/ahriman/version.py
sed -i 's/^__version__ = .*/__version__ = "$(VERSION)"/' src/ahriman/__init__.py

View File

@ -16,6 +16,7 @@ Wrapper for managing custom repository inspired by [repo-scripts](https://github
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support.
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options:

View File

@ -43,6 +43,9 @@ fi
if [ -n "$AHRIMAN_PORT" ]; then
AHRIMAN_SETUP_ARGS+=("--web-port" "$AHRIMAN_PORT")
fi
if [ -n "$AHRIMAN_REPOSITORY_SERVER" ]; then
AHRIMAN_SETUP_ARGS+=("--server" "$AHRIMAN_REPOSITORY_SERVER")
fi
if [ -n "$AHRIMAN_UNIX_SOCKET" ]; then
AHRIMAN_SETUP_ARGS+=("--web-unix-socket" "$AHRIMAN_UNIX_SOCKET")
fi
@ -59,7 +62,7 @@ systemd-machine-id-setup &> /dev/null
if [ -n "$AHRIMAN_FORCE_ROOT" ]; then
AHRIMAN_EXECUTABLE=("ahriman")
elif ahriman help-commands-unsafe -- "$@" &> /dev/null; then
AHRIMAN_EXECUTABLE=("sudo" "-u" "$AHRIMAN_USER" "--" "ahriman")
AHRIMAN_EXECUTABLE=("sudo" "-E" "-u" "$AHRIMAN_USER" "--" "ahriman")
else
AHRIMAN_EXECUTABLE=("ahriman")
fi

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 829 KiB

After

Width:  |  Height:  |  Size: 839 KiB

View File

@ -20,6 +20,14 @@ ahriman.core.configuration.schema module
:no-undoc-members:
:show-inheritance:
ahriman.core.configuration.shell\_interpolator module
-----------------------------------------------------
.. automodule:: ahriman.core.configuration.shell_interpolator
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.configuration.validator module
-------------------------------------------

View File

@ -76,6 +76,22 @@ ahriman.core.database.migrations.m008\_packagers module
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m009\_local\_source module
-----------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m009_local_source
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.database.migrations.m010\_version\_based\_logs\_removal module
---------------------------------------------------------------------------
.. automodule:: ahriman.core.database.migrations.m010_version_based_logs_removal
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -0,0 +1,21 @@
ahriman.core.http package
=========================
Submodules
----------
ahriman.core.http.sync\_http\_client module
-------------------------------------------
.. automodule:: ahriman.core.http.sync_http_client
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: ahriman.core.http
:members:
:no-undoc-members:
:show-inheritance:

View File

@ -36,6 +36,14 @@ ahriman.core.report.jinja\_template module
:no-undoc-members:
:show-inheritance:
ahriman.core.report.remote\_call module
---------------------------------------
.. automodule:: ahriman.core.report.remote_call
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.report.report module
---------------------------------

View File

@ -14,6 +14,7 @@ Subpackages
ahriman.core.database
ahriman.core.formatters
ahriman.core.gitremote
ahriman.core.http
ahriman.core.log
ahriman.core.report
ahriman.core.repository

View File

@ -20,6 +20,14 @@ ahriman.core.upload.http\_upload module
:no-undoc-members:
:show-inheritance:
ahriman.core.upload.remote\_service module
------------------------------------------
.. automodule:: ahriman.core.upload.remote_service
:members:
:no-undoc-members:
:show-inheritance:
ahriman.core.upload.rsync module
--------------------------------

View File

@ -220,6 +220,14 @@ ahriman.models.user\_access module
:no-undoc-members:
:show-inheritance:
ahriman.models.waiter module
----------------------------
.. automodule:: ahriman.models.waiter
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -12,17 +12,6 @@ Subpackages
ahriman.models
ahriman.web
Submodules
----------
ahriman.version module
----------------------
.. automodule:: ahriman.version
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -36,6 +36,14 @@ ahriman.web.schemas.error\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.file\_schema module
---------------------------------------
.. automodule:: ahriman.web.schemas.file_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.internal\_status\_schema module
---------------------------------------------------
@ -132,6 +140,22 @@ ahriman.web.schemas.pgp\_key\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.process\_id\_schema module
----------------------------------------------
.. automodule:: ahriman.web.schemas.process_id_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.process\_schema module
------------------------------------------
.. automodule:: ahriman.web.schemas.process_schema
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.remote\_schema module
-----------------------------------------
@ -156,6 +180,14 @@ ahriman.web.schemas.status\_schema module
:no-undoc-members:
:show-inheritance:
ahriman.web.schemas.update\_flags\_schema module
------------------------------------------------
.. automodule:: ahriman.web.schemas.update_flags_schema
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -20,6 +20,14 @@ ahriman.web.views.service.pgp module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.process module
----------------------------------------
.. automodule:: ahriman.web.views.service.process
:members:
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.rebuild module
----------------------------------------
@ -60,6 +68,14 @@ ahriman.web.views.service.update module
:no-undoc-members:
:show-inheritance:
ahriman.web.views.service.upload module
---------------------------------------
.. automodule:: ahriman.web.views.service.upload
:members:
:no-undoc-members:
:show-inheritance:
Module contents
---------------

View File

@ -37,6 +37,7 @@ This package contains everything required for the most of application actions an
* ``ahriman.core.database`` is everything including data and schema migrations for database.
* ``ahriman.core.formatters`` package provides ``Printer`` sub-classes for printing data (e.g. package properties) to stdout which are used by some handlers.
* ``ahriman.core.gitremote`` is a package with remote PKGBUILD triggers. Should not be called directly.
* ``ahriman.core.http`` package provides HTTP clients which can be later used by other classes.
* ``ahriman.core.log`` is a log utils package. It includes logger loader class, custom HTTP based logger and access logger for HTTP services with additional filters.
* ``ahriman.core.report`` is a package with reporting triggers. Should not be called directly.
* ``ahriman.core.repository`` contains several traits and base repository (``ahriman.core.repository.Repository`` class) implementation.
@ -168,6 +169,7 @@ This feature is divided into to stages: check AUR for updates and run rebuild fo
#. For each level of tree it does:
#. Download package data from AUR.
#. Bump ``pkgrel`` if there is duplicate version in the local repository (see explanation below).
#. Build every package in clean chroot.
#. Sign packages if required.
#. Add packages to database and sign database if required.
@ -175,6 +177,20 @@ This feature is divided into to stages: check AUR for updates and run rebuild fo
After any step any package data is being removed.
pkgrel bump rules
^^^^^^^^^^^^^^^^^
The application is able to automatically bump package release (``pkgrel``) during build process if there is duplicate version in repository. The version will be incremented as following:
#. Get version of the remote package.
#. Get version of the local package if any.
#. If local version is not set, proceed with remote one.
#. If local version is set and epoch or package version (``pkgver``) are different, proceed with remote version.
#. If local version is set and remote version is newer than local one, proceed with remote.
#. Extract ``pkgrel`` value.
#. If it has ``major.minor`` notation (e.g. ``1.1``), then increment last part by 1, e.g. ``1.1 -> 1.2``, ``1.0.1 -> 1.0.2``.
#. If ``pkgrel`` is a number (e.g. ``1``), then append 1 to the end of the string, e.g. ``1 -> 1.1``.
Core functions reference
------------------------
@ -216,7 +232,7 @@ The package provides several authorization methods: disabled, based on configura
Disabled (default) authorization provider just allows everything for everyone and does not have any specific configuration (it uses some default configuration parameters though). It also provides generic interface for derived classes.
Mapping (aka configuration) provider uses hashed passwords with salt from the database in order to authenticate users. This provider also enables user permission checking (read/write) (authorization). Thus, it defines the following methods:
Mapping (aka configuration) provider uses hashed passwords with optional salt from the database in order to authenticate users. This provider also enables user permission checking (read/write) (authorization). Thus, it defines the following methods:
* ``check_credentials`` - user password validation (authentication).
* ``verify_access`` - user permission validation (authorization).

View File

@ -15,7 +15,7 @@ import sys
from pathlib import Path
from ahriman.version import __version__
from ahriman import __version__
basedir = Path(__file__).resolve().parent.parent / "src"
@ -42,6 +42,7 @@ release = __version__
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.napoleon",
"sphinx_rtd_theme",
"sphinxarg.ext",
]
@ -66,7 +67,7 @@ exclude_patterns = []
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "default" if on_rtd else "alabaster"
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,

View File

@ -12,6 +12,15 @@ There are two variable types which have been added to default ones, they are pat
Path values, except for casting to ``pathlib.Path`` type, will be also expanded to absolute paths relative to the configuration path. E.g. if path is set to ``ahriman.ini.d/logging.ini`` and root configuration path is ``/etc/ahriman.ini``, the value will be expanded to ``/etc/ahriman.ini.d/logging.ini``. In order to disable path expand, use the full path, e.g. ``/etc/ahriman.ini.d/logging.ini``.
Configuration allows string interpolation from environment variables, e.g.:
.. code-block:: ini
[section]
key = $SECRET
will try to read value from ``SECRET`` environment variable. In case if the required environment variable wasn't found, it will keep original value (i.e. ``$SECRET`` in the example). Dollar sign can be set as ``$$``.
There is also additional subcommand which will allow to validate configuration and print found errors. In order to do so, run ``service-config-validate`` subcommand, e.g.:
.. code-block:: shell
@ -54,7 +63,7 @@ Base authorization settings. ``OAuth`` provider requires ``aioauth-client`` libr
* ``max_age`` - parameter which controls both cookie expiration and token expiration inside the service, integer, optional, default is 7 days.
* ``oauth_provider`` - OAuth2 provider class name as is in ``aioauth-client`` (e.g. ``GoogleClient``, ``GithubClient`` etc), string, required in case if ``oauth`` is used.
* ``oauth_scopes`` - scopes list for OAuth2 provider, which will allow retrieving user email (which is used for checking user permissions), e.g. ``https://www.googleapis.com/auth/userinfo.email`` for ``GoogleClient`` or ``user:email`` for ``GithubClient``, space separated list of strings, required in case if ``oauth`` is used.
* ``salt`` - password hash salt, string, required in case if authorization enabled (automatically generated by ``user-add`` subcommand).
* ``salt`` - additional password hash salt, string, optional.
Authorized users are stored inside internal database, if any of external provides are used the password field for non-service users must be empty.
@ -97,15 +106,19 @@ Web server settings. If any of ``host``/``port`` is not set, web integration wil
* ``debug`` - enable debug toolbar, boolean, optional, default ``no``.
* ``debug_check_host`` - check hosts to access debug toolbar, boolean, optional, default ``no``.
* ``debug_allowed_hosts`` - allowed hosts to get access to debug toolbar, space separated list of string, optional.
* ``enable_archive_upload`` - allow to upload packages via HTTP (i.e. call of ``/api/v1/service/upload`` uri), boolean, optional, default ``no``.
* ``host`` - host to bind, string, optional.
* ``index_url`` - full url of the repository index page, string, optional.
* ``max_body_size`` - max body size in bytes to be validated for archive upload, integer, optional. If not set, validation will be disabled.
* ``password`` - password to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``port`` - port to bind, int, optional.
* ``static_path`` - path to directory with static files, string, required.
* ``templates`` - path to templates directory, string, required.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``unix_socket`` - path to the listening unix socket, string, optional. If set, server will create the socket on the specified address which can (and will) be used by application. Note, that unlike usual host/port configuration, unix socket allows to perform requests without authorization.
* ``unix_socket_unsafe`` - set unsafe (o+w) permissions to unix socket, boolean, optional, default ``yes``. This option is enabled by default, because it is supposed that unix socket is created in safe environment (only web service is supposed to be used in unsafe), but it can be disabled by configuration.
* ``username`` - username to authorize in web service in order to update service status, string, required in case if authorization enabled.
* ``wait_timeout`` - wait timeout in seconds, maximum amount of time to be waited before lock will be free, int, optional.
``keyring`` group
--------------------
@ -117,6 +130,7 @@ Keyring package generator plugin.
Keyring generator plugin
^^^^^^^^^^^^^^^^^^^^^^^^
* ``type`` - type of the generator, string, optional, must be set to ``keyring-generator`` if exists.
* ``description`` - keyring package description, string, optional, default is ``repo PGP keyring``, where ``repo`` is the repository name.
* ``homepage`` - url to homepage location if any, string, optional.
* ``license`` - list of licenses which are applied to this package, space separated list of strings, optional, default is ``Unlicense``.
@ -135,6 +149,7 @@ Mirrorlist package generator plugin.
Mirrorlist generator plugin
^^^^^^^^^^^^^^^^^^^^^^^^^^^
* ``type`` - type of the generator, string, optional, must be set to ``mirrorlist-generator`` if exists.
* ``description`` - mirrorlist package description, string, optional, default is ``repo mirror list for use by pacman``, where ``repo`` is the repository name.
* ``homepage`` - url to homepage location if any, string, optional.
* ``license`` - list of licenses which are applied to this package, space separated list of strings, optional, default is ``Unlicense``.
@ -179,7 +194,8 @@ Available options are:
Remote push trigger
^^^^^^^^^^^^^^^^^^^
* ``commit_author`` - git commit author, string, optional. In case if not set, the git will generate author for you. Note, however, that in this case it will disclosure your hostname.
* ``commit_email`` - git commit email, string, optional, default is ``ahriman@localhost``.
* ``commit_user`` - git commit user, string, optional, default is ``ahriman``.
* ``push_url`` - url of the remote repository to which PKGBUILDs should be pushed after build process, string, required.
* ``push_branch`` - branch of the remote repository to which PKGBUILDs should be pushed after build process, string, optional, default is ``master``.
@ -234,6 +250,17 @@ Section name must be either ``html`` (plus optional architecture name, e.g. ``ht
* ``path`` - path to html report file, string, required.
* ``template_path`` - path to Jinja2 template, string, required.
``remote-call`` type
^^^^^^^^^^^^^^^^^^^^
Section name must be either ``remote-call`` (plus optional architecture name, e.g. ``remote-call:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``remote-call`` if exists.
* ``aur`` - check for AUR packages updates, boolean, optional, default ``no``.
* ``local`` - check for local packages updates, boolean, optional, default ``no``.
* ``manual`` - update manually built packages, boolean, optional, default ``no``.
* ``wait_timeout`` - maximum amount of time in seconds to be waited before remote process will be terminated, int, optional, default ``-1``.
``telegram`` type
^^^^^^^^^^^^^^^^^
@ -279,6 +306,14 @@ This feature requires Github key creation (see below). Section name must be eith
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
* ``username`` - Github authorization user, string, required. Basically the same as ``owner``.
``remote-service`` type
^^^^^^^^^^^^^^^^^^^^^^^
Section name must be either ``remote-service`` (plus optional architecture name, e.g. ``remote-service:x86_64``) or random name with ``type`` set.
* ``type`` - type of the report, string, optional, must be set to ``remote-service`` if exists.
* ``timeout`` - HTTP request timeout in seconds, int, optional, default is ``30``.
``rsync`` type
^^^^^^^^^^^^^^

View File

@ -305,7 +305,7 @@ TL;DR
sudo -u ahriman ahriman repo-rebuild --depends-on python
You can even rebuild the whole repository (which is particular useful in case if you would like to change packager) if you do not supply ``--depends-on`` option.
You can even rebuild the whole repository (which is particular useful in case if you would like to change packager) if you do not supply ``--depends-on`` option. This action will automatically increment ``pkgrel`` value; in case if you don't want to, the ``--no-increment`` option has to be supplied.
However, note that you do not need to rebuild repository in case if you just changed signing option, just use ``repo-sign`` command instead.
@ -396,6 +396,7 @@ The following environment variables are supported:
* ``AHRIMAN_PACMAN_MIRROR`` - override pacman mirror server if set.
* ``AHRIMAN_PORT`` - HTTP server port if any, default is empty.
* ``AHRIMAN_REPOSITORY`` - repository name, default is ``aur-clone``.
* ``AHRIMAN_REPOSITORY_SERVER`` - optional override for the repository url. Useful if you would like to download packages from remote instead of local filesystem.
* ``AHRIMAN_REPOSITORY_ROOT`` - repository root. Because of filesystem rights it is required to override default repository root. By default, it uses ``ahriman`` directory inside ahriman's home, which can be passed as mount volume.
* ``AHRIMAN_UNIX_SOCKET`` - full path to unix socket which is used by web server, default is empty. Note that more likely you would like to put it inside ``AHRIMAN_REPOSITORY_ROOT`` directory (e.g. ``/var/lib/ahriman/ahriman/ahriman-web.sock``) or to ``/tmp``.
* ``AHRIMAN_USER`` - ahriman user, usually must not be overwritten, default is ``ahriman``.
@ -722,8 +723,7 @@ How to post build report to telegram
#.
Optionally (if you want to post message in chat):
#. Create telegram channel.
#. Create telegram channel.
#. Invite your bot into the channel.
#. Make your channel public
@ -753,6 +753,203 @@ If you did everything fine you should receive the message with the next update.
(replace ``${CHAT_ID}`` and ``${API_KEY}`` with the values from configuration).
Distributed builds
------------------
The service allows to run build on multiple machines and collect packages on main node. There are multiple ways to achieve it, this section describes officially supported methods.
Remote synchronization and remote server call
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This setup requires at least two instances of the service:
#. Web service (with opt-in authorization enabled), later will be referenced as ``master`` node.
#. Application instances responsible for build, later will be referenced as ``worker`` nodes.
In this example the following settings are assumed:
* Repository architecture is ``x86_64``.
* Master node address is ``master.example.com``.
Master node configuration
"""""""""""""""""""""""""
The only requirements for the master node is that API must be available for worker nodes to call (e.g. port must be exposed to internet, or local network in case of VPN, etc) and file upload must be enabled:
.. code-block:: ini
[web]
enable_archive_upload = yes
In addition, the following settings are recommended for the master node:
*
As it has been mentioned above, it is recommended to enable authentication (see `How to enable basic authorization`_) and create system user which will be used later. Later this user (if any) will be referenced as ``worker-user``.
*
In order to be able to spawn multiple processes at the same time, wait timeout must be configured:
.. code-block:: ini
[web]
wait_timeout = 0
Worker nodes configuration
""""""""""""""""""""""""""
#.
First of all, in this setup you need to split your repository into chunks manually, e.g. if you have repository on master node with packages ``A``, ``B`` and ``C``, you need to split them between all available workers, as example:
* Worker #1: ``A``.
* Worker #2: ``B`` and ``C``.
#.
Each worker must be configured to upload files to master node:
.. code-block:: ini
[upload]
target = remote-service
[remote-service]
#.
Worker must be configured to access web on master node:
.. code-block:: ini
[web]
address = master.example.com
username = worker-user
password = very-secure-password
As it has been mentioned above, ``web.address`` must be available for workers. In case if unix socket is used, it can be passed as ``web.unix_socket`` variable as usual. Optional ``web.username``/``web.password`` can be supplied in case if authentication was enabled on master node.
#.
Each worker must call master node on success:
.. code-block:: ini
[report]
target = remote-call
[remote-call]
manual = yes
After success synchronization (see above), the built packages will be put into directory, from which they will be read during manual update, thus ``remote-call.manual`` flag is required.
#.
Change order of trigger runs. This step is required, because by default the report trigger is called before the upload trigger and we would like to achieve the opposite:
.. code-block:: ini
[build]
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.upload.UploadTrigger ahriman.core.report.ReportTrigger ahriman.core.gitremote.RemotePushTrigger
In addition, the following settings are recommended for workers:
*
You might want to wait until report trigger will be completed; in this case the following option must be set:
.. code-block:: ini
[remote-call]
wait_timeout = 0
Dependency management
"""""""""""""""""""""
By default worker nodes don't know anything about master nodes packages, thus it will try to build each dependency by its own. However, using ``AHRIMAN_REPOSITORY_SERVER`` docker variable (or ``--server`` flag for setup command), it is possible to specify address of the master node for devtools configuration.
Repository and packages signing
"""""""""""""""""""""""""""""""
You can sign packages on worker nodes and then signatures will be synced to master node. In order to do so, you need to configure worker node as following, e.g.:
.. code-block:: ini
[sign]
target = package
key = 8BE91E5A773FB48AC05CC1EDBED105AED6246B39
Note, however, that in this case, signatures will not be validated on master node and just will be copied to repository tree.
If you would like to sign only database files (aka repository sign), it has to be configured on master node only as usual, e.g.:
.. code-block:: ini
[sign]
target = repository
key = 8BE91E5A773FB48AC05CC1EDBED105AED6246B39
Double node minimal docker example
""""""""""""""""""""""""""""""""""
Master node config (``master.ini``) as:
.. code-block:: ini
[auth]
target = mapping
[web]
enable_archive_upload = yes
wait_timeout = 0
Command to run master node:
.. code-block:: shell
docker run --privileged -p 8080:8080 -e AHRIMAN_PORT=8080 -v master.ini:/etc/ahriman.ini.d/overrides.ini arcan1s/ahriman:latest web
The user ``worker-user`` has been created additionally. Worker node config (``worker.ini``) as:
.. code-block:: ini
[web]
address = http://172.17.0.1:8080
username = worker-user
password = very-secure-password
[upload]
target = remote-service
[remote-service]
[report]
target = remote-call
[remote-call]
manual = yes
wait_timeout = 0
[build]
triggers = ahriman.core.gitremote.RemotePullTrigger ahriman.core.upload.UploadTrigger ahriman.core.report.ReportTrigger ahriman.core.gitremote.RemotePushTrigger
The address above (``http://172.17.0.1:8080``) is something available for worker container.
Command to run worker node:
.. code-block:: shell
docker run --privileged -v worker.ini:/etc/ahriman.ini.d/overrides.ini -it arcan1s/ahriman:latest package-add arhiman --now
The command above will successfully build ``ahriman`` package, upload it on master node and, finally, will update master node repository.
Addition of new package and repository update
"""""""""""""""""""""""""""""""""""""""""""""
Just run on worker command as usual, the built packages will be automatically uploaded to master node. Note that automatic update process must be disabled on master node.
Package removal
"""""""""""""""
This action must be done in two steps:
#. Remove package on worker.
#. Remove package on master node.
Maintenance packages
--------------------
@ -767,7 +964,7 @@ The application provides special plugin which generates keyring package. This pl
.. code-block:: ini
[keyring]
target = keyring_generator
target = keyring-generator
By default it will use ``sign.key`` as trusted key and all other keys as packagers ones. For all available options refer to :doc:`configuration <configuration>`.
@ -802,12 +999,12 @@ The application provides special plugin which generates mirrorlist package also.
.. code-block:: ini
[mirrorlist]
target = mirrorlist_generator
target = mirrorlist-generator
[mirrorlist_generator]
[mirrorlist-generator]
servers = https://repo.example.com/$arch
The ``mirrorlist_generator.servers`` must contain list of available mirrors, the ``$arch`` and ``$repo`` variables are supported. For more options kindly refer to :doc:`configuration <configuration>`.
The ``mirrorlist-generator.servers`` must contain list of available mirrors, the ``$arch`` and ``$repo`` variables are supported. For more options kindly refer to :doc:`configuration <configuration>`.
#.
Create package source files:
@ -870,6 +1067,8 @@ How to enable basic authorization
target = configuration
salt = somerandomstring
The ``salt`` parameter is optional, but recommended.
#.
In order to provide access for reporting from application instances you can (recommended way) use unix sockets by configuring the following (note, that it requires ``python-requests-unixsocket`` package to be installed):
@ -934,7 +1133,7 @@ How to enable OAuth authorization
Configure ``oauth_provider`` and ``oauth_scopes`` in case if you would like to use different from Google provider. Scope must grant access to user email. ``web.address`` is required to make callback URL available from internet.
#.
If you are not going to use unix socket, you also need to create service user (remember to set ``auth.salt`` option before):
If you are not going to use unix socket, you also need to create service user (remember to set ``auth.salt`` option before if required):
.. code-block:: shell

View File

@ -12,6 +12,7 @@ Features
* VCS packages support.
* Official repository support.
* Ability to patch AUR packages and even create package from local PKGBUILDs.
* Various rebuild options with ability to automatically bump package version.
* Sign support with gpg (repository, package), multiple packagers support.
* Triggers for repository updates, e.g. synchronization to remote services (rsync, s3 and github) and report generation (email, html, telegram).
* Repository status interface with optional authorization and control options.

View File

@ -1,14 +1,14 @@
# Maintainer: Evgeniy Alekseev
pkgname='ahriman'
pkgver=2.10.2
pkgver=2.11.0
pkgrel=1
pkgdesc="ArcH linux ReposItory MANager"
arch=('any')
url="https://github.com/arcan1s/ahriman"
license=('GPL3')
depends=('devtools>=1:1.0.0' 'git' 'pyalpm' 'python-cerberus' 'python-inflection' 'python-passlib' 'python-requests' 'python-srcinfo')
makedepends=('python-build' 'python-installer' 'python-wheel')
makedepends=('python-build' 'python-flit' 'python-installer' 'python-wheel')
optdepends=('breezy: -bzr packages support'
'darcs: -darcs packages support'
'mercurial: -hg packages support'
@ -45,8 +45,10 @@ package() {
python -m installer --destdir="$pkgdir" "dist/$pkgname-$pkgver-py3-none-any.whl"
# python-installer actually thinks that you cannot just copy files to root
# thus we need to copy them manually
# thanks too PEP517, which we all wanted, you need to install data files manually nowadays
pushd package && find . -type f -exec install -Dm644 "{}" "$pkgdir/usr/{}" \; && popd
# keep usr/share configs as reference and copy them to /etc
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini" "$pkgdir/etc/ahriman.ini"
install -Dm644 "$pkgdir/usr/share/$pkgname/settings/ahriman.ini.d/logging.ini" "$pkgdir/etc/ahriman.ini.d/logging.ini"

View File

@ -1,3 +0,0 @@
#!/bin/sh
exec python -B -m ahriman.application.ahriman "$@"

View File

@ -112,7 +112,7 @@
const payload = response.map(description => {
const package_base = description.package.base;
const web_url = description.package.remote?.web_url;
const web_url = description.package.remote.web_url;
return {
id: package_base,
base: web_url ? `<a href="${safe(web_url)}" title="${safe(package_base)}">${safe(package_base)}</a>` : safe(package_base),

View File

@ -2,7 +2,7 @@
_shtab_ahriman_subparsers=('aur-search' 'search' 'help' 'help-commands-unsafe' 'help-updates' 'help-version' 'version' 'package-add' 'add' 'package-update' 'package-remove' 'remove' 'package-status' 'status' 'package-status-remove' 'package-status-update' 'status-update' 'patch-add' 'patch-list' 'patch-remove' 'patch-set-add' 'repo-backup' 'repo-check' 'check' 'repo-create-keyring' 'repo-create-mirrorlist' 'repo-daemon' 'daemon' 'repo-rebuild' 'rebuild' 'repo-remove-unknown' 'remove-unknown' 'repo-report' 'report' 'repo-restore' 'repo-sign' 'sign' 'repo-status-update' 'repo-sync' 'sync' 'repo-tree' 'repo-triggers' 'repo-update' 'update' 'service-clean' 'clean' 'repo-clean' 'service-config' 'config' 'repo-config' 'service-config-validate' 'config-validate' 'repo-config-validate' 'service-key-import' 'key-import' 'service-setup' 'init' 'repo-init' 'repo-setup' 'setup' 'service-shell' 'shell' 'user-add' 'user-list' 'user-remove' 'web')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '--report' '--no-report' '-q' '--quiet' '--unsafe' '-V' '--version')
_shtab_ahriman_option_strings=('-h' '--help' '-a' '--architecture' '-c' '--configuration' '--force' '-l' '--lock' '--log-handler' '--report' '--no-report' '-q' '--quiet' '--unsafe' '--wait-timeout' '-V' '--version')
_shtab_ahriman_aur_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_search_option_strings=('-h' '--help' '-e' '--exit-code' '--info' '--no-info' '--sort-by')
_shtab_ahriman_help_option_strings=('-h' '--help')
@ -10,9 +10,9 @@ _shtab_ahriman_help_commands_unsafe_option_strings=('-h' '--help')
_shtab_ahriman_help_updates_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_help_version_option_strings=('-h' '--help')
_shtab_ahriman_version_option_strings=('-h' '--help')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_add_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_update_option_strings=('-h' '--help' '--dependencies' '--no-dependencies' '-e' '--exit-code' '--increment' '--no-increment' '-n' '--now' '-y' '--refresh' '-s' '--source' '-u' '--username')
_shtab_ahriman_package_remove_option_strings=('-h' '--help')
_shtab_ahriman_remove_option_strings=('-h' '--help')
_shtab_ahriman_package_status_option_strings=('-h' '--help' '--ahriman' '-e' '--exit-code' '--info' '--no-info' '-s' '--status')
@ -31,8 +31,8 @@ _shtab_ahriman_repo_create_keyring_option_strings=('-h' '--help')
_shtab_ahriman_repo_create_mirrorlist_option_strings=('-h' '--help')
_shtab_ahriman_repo_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_daemon_option_strings=('-h' '--help' '-i' '--interval' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--local' '--no-local' '--manual' '--no-manual' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_repo_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_rebuild_option_strings=('-h' '--help' '--depends-on' '--dry-run' '--from-database' '--increment' '--no-increment' '-e' '--exit-code' '-s' '--status' '-u' '--username')
_shtab_ahriman_repo_remove_unknown_option_strings=('-h' '--help' '--dry-run')
_shtab_ahriman_remove_unknown_option_strings=('-h' '--help' '--dry-run')
_shtab_ahriman_repo_report_option_strings=('-h' '--help')
@ -43,10 +43,10 @@ _shtab_ahriman_sign_option_strings=('-h' '--help')
_shtab_ahriman_repo_status_update_option_strings=('-h' '--help' '-s' '--status')
_shtab_ahriman_repo_sync_option_strings=('-h' '--help')
_shtab_ahriman_sync_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help')
_shtab_ahriman_repo_tree_option_strings=('-h' '--help' '-p' '--partitions')
_shtab_ahriman_repo_triggers_option_strings=('-h' '--help')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_repo_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_update_option_strings=('-h' '--help' '--aur' '--no-aur' '--dependencies' '--no-dependencies' '--dry-run' '-e' '--exit-code' '--increment' '--no-increment' '--local' '--no-local' '--manual' '--no-manual' '-u' '--username' '--vcs' '--no-vcs' '-y' '--refresh')
_shtab_ahriman_service_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
_shtab_ahriman_repo_clean_option_strings=('-h' '--help' '--cache' '--no-cache' '--chroot' '--no-chroot' '--manual' '--no-manual' '--packages' '--no-packages' '--pacman' '--no-pacman')
@ -58,11 +58,11 @@ _shtab_ahriman_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_repo_config_validate_option_strings=('-h' '--help' '-e' '--exit-code')
_shtab_ahriman_service_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_key_import_option_strings=('-h' '--help' '--key-server')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_init_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_repo_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_setup_option_strings=('-h' '--help' '--build-as-user' '--build-command' '--from-configuration' '--generate-salt' '--no-generate-salt' '--makeflags-jobs' '--no-makeflags-jobs' '--mirror' '--multilib' '--no-multilib' '--packager' '--repository' '--server' '--sign-key' '--sign-target' '--web-port' '--web-unix-socket')
_shtab_ahriman_service_shell_option_strings=('-h' '--help')
_shtab_ahriman_shell_option_strings=('-h' '--help')
_shtab_ahriman_user_add_option_strings=('-h' '--help' '--key' '--packager' '-p' '--password' '-r' '--role')
@ -151,6 +151,8 @@ _shtab_ahriman_package_add___dependencies_nargs=0
_shtab_ahriman_package_add___no_dependencies_nargs=0
_shtab_ahriman_package_add__e_nargs=0
_shtab_ahriman_package_add___exit_code_nargs=0
_shtab_ahriman_package_add___increment_nargs=0
_shtab_ahriman_package_add___no_increment_nargs=0
_shtab_ahriman_package_add__n_nargs=0
_shtab_ahriman_package_add___now_nargs=0
_shtab_ahriman_package_add__y_nargs=0
@ -162,6 +164,8 @@ _shtab_ahriman_add___dependencies_nargs=0
_shtab_ahriman_add___no_dependencies_nargs=0
_shtab_ahriman_add__e_nargs=0
_shtab_ahriman_add___exit_code_nargs=0
_shtab_ahriman_add___increment_nargs=0
_shtab_ahriman_add___no_increment_nargs=0
_shtab_ahriman_add__n_nargs=0
_shtab_ahriman_add___now_nargs=0
_shtab_ahriman_add__y_nargs=0
@ -173,6 +177,8 @@ _shtab_ahriman_package_update___dependencies_nargs=0
_shtab_ahriman_package_update___no_dependencies_nargs=0
_shtab_ahriman_package_update__e_nargs=0
_shtab_ahriman_package_update___exit_code_nargs=0
_shtab_ahriman_package_update___increment_nargs=0
_shtab_ahriman_package_update___no_increment_nargs=0
_shtab_ahriman_package_update__n_nargs=0
_shtab_ahriman_package_update___now_nargs=0
_shtab_ahriman_package_update__y_nargs=0
@ -274,12 +280,16 @@ _shtab_ahriman_repo_rebuild__h_nargs=0
_shtab_ahriman_repo_rebuild___help_nargs=0
_shtab_ahriman_repo_rebuild___dry_run_nargs=0
_shtab_ahriman_repo_rebuild___from_database_nargs=0
_shtab_ahriman_repo_rebuild___increment_nargs=0
_shtab_ahriman_repo_rebuild___no_increment_nargs=0
_shtab_ahriman_repo_rebuild__e_nargs=0
_shtab_ahriman_repo_rebuild___exit_code_nargs=0
_shtab_ahriman_rebuild__h_nargs=0
_shtab_ahriman_rebuild___help_nargs=0
_shtab_ahriman_rebuild___dry_run_nargs=0
_shtab_ahriman_rebuild___from_database_nargs=0
_shtab_ahriman_rebuild___increment_nargs=0
_shtab_ahriman_rebuild___no_increment_nargs=0
_shtab_ahriman_rebuild__e_nargs=0
_shtab_ahriman_rebuild___exit_code_nargs=0
_shtab_ahriman_repo_remove_unknown__h_nargs=0
@ -321,6 +331,8 @@ _shtab_ahriman_repo_update___no_dependencies_nargs=0
_shtab_ahriman_repo_update___dry_run_nargs=0
_shtab_ahriman_repo_update__e_nargs=0
_shtab_ahriman_repo_update___exit_code_nargs=0
_shtab_ahriman_repo_update___increment_nargs=0
_shtab_ahriman_repo_update___no_increment_nargs=0
_shtab_ahriman_repo_update___local_nargs=0
_shtab_ahriman_repo_update___no_local_nargs=0
_shtab_ahriman_repo_update___manual_nargs=0
@ -339,6 +351,8 @@ _shtab_ahriman_update___no_dependencies_nargs=0
_shtab_ahriman_update___dry_run_nargs=0
_shtab_ahriman_update__e_nargs=0
_shtab_ahriman_update___exit_code_nargs=0
_shtab_ahriman_update___increment_nargs=0
_shtab_ahriman_update___no_increment_nargs=0
_shtab_ahriman_update___local_nargs=0
_shtab_ahriman_update___no_local_nargs=0
_shtab_ahriman_update___manual_nargs=0

View File

@ -1,9 +1,9 @@
.TH AHRIMAN "1" "2023\-08\-07" "ahriman" "Generated Python Manual"
.TH AHRIMAN "1" "2023\-08\-26" "ahriman" "Generated Python Manual"
.SH NAME
ahriman
.SH SYNOPSIS
.B ahriman
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [--report | --no-report] [-q] [--unsafe] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,user-add,user-list,user-remove,web} ...
[-h] [-a ARCHITECTURE] [-c CONFIGURATION] [--force] [-l LOCK] [--log-handler {console,syslog,journald}] [--report | --no-report] [-q] [--unsafe] [--wait-timeout WAIT_TIMEOUT] [-V] {aur-search,search,help,help-commands-unsafe,help-updates,help-version,version,package-add,add,package-update,package-remove,remove,package-status,status,package-status-remove,package-status-update,status-update,patch-add,patch-list,patch-remove,patch-set-add,repo-backup,repo-check,check,repo-create-keyring,repo-create-mirrorlist,repo-daemon,daemon,repo-rebuild,rebuild,repo-remove-unknown,remove-unknown,repo-report,report,repo-restore,repo-sign,sign,repo-status-update,repo-sync,sync,repo-tree,repo-triggers,repo-update,update,service-clean,clean,repo-clean,service-config,config,repo-config,service-config-validate,config-validate,repo-config-validate,service-key-import,key-import,service-setup,init,repo-init,repo-setup,setup,service-shell,shell,user-add,user-list,user-remove,web} ...
.SH DESCRIPTION
ArcH linux ReposItory MANager
@ -40,6 +40,11 @@ force disable any logging
\fB\-\-unsafe\fR
allow to run ahriman as non\-ahriman user. Some actions might be unavailable
.TP
\fB\-\-wait\-timeout\fR \fI\,WAIT_TIMEOUT\/\fR
wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of
zero value, the application will wait infinitely
.TP
\fB\-V\fR, \fB\-\-version\fR
show program's version number and exit
@ -224,7 +229,7 @@ usage: ahriman help\-version [\-h]
print application and its dependencies versions
.SH COMMAND \fI\,'ahriman package\-add'\/\fR
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-n] [\-y]
usage: ahriman package\-add [\-h] [\-\-dependencies | \-\-no\-dependencies] [\-e] [\-\-increment | \-\-no\-increment] [\-n] [\-y]
[\-s {auto,archive,aur,directory,local,remote,repository}] [\-u USERNAME]
package [package ...]
@ -243,6 +248,10 @@ process missing package dependencies
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-\-increment\fR, \fB\-\-no\-increment\fR
increment package release (pkgrel) version on duplicate
.TP
\fB\-n\fR, \fB\-\-now\fR
run update function after
@ -459,8 +468,8 @@ fetch actual version of VCS packages
download fresh package databases from the mirror before actions, \-yy to force refresh even if up to date
.SH COMMAND \fI\,'ahriman repo\-rebuild'\/\fR
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-e]
[\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
usage: ahriman repo\-rebuild [\-h] [\-\-depends\-on DEPENDS_ON] [\-\-dry\-run] [\-\-from\-database] [\-\-increment | \-\-no\-increment]
[\-e] [\-s {unknown,pending,building,failed,success}] [\-u USERNAME]
force rebuild whole repository
@ -479,6 +488,10 @@ read packages from database instead of filesystem. This feature in particular is
restore repository from another repository instance. Note, however, that in order to restore packages you need to have
original ahriman instance run with web service and have run repo\-update at least once.
.TP
\fB\-\-increment\fR, \fB\-\-no\-increment\fR
increment package release (pkgrel) on duplicate
.TP
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
@ -545,10 +558,15 @@ usage: ahriman repo\-sync [\-h]
sync repository files to remote server according to current settings
.SH COMMAND \fI\,'ahriman repo\-tree'\/\fR
usage: ahriman repo\-tree [\-h]
usage: ahriman repo\-tree [\-h] [\-p PARTITIONS]
dump repository tree based on packages dependencies
.SH OPTIONS \fI\,'ahriman repo\-tree'\/\fR
.TP
\fB\-p\fR \fI\,PARTITIONS\/\fR, \fB\-\-partitions\fR \fI\,PARTITIONS\/\fR
also divide packages by independent partitions
.SH COMMAND \fI\,'ahriman repo\-triggers'\/\fR
usage: ahriman repo\-triggers [\-h] [trigger ...]
@ -560,7 +578,8 @@ instead of running all triggers as set by configuration, just process specified
.SH COMMAND \fI\,'ahriman repo\-update'\/\fR
usage: ahriman repo\-update [\-h] [\-\-aur | \-\-no\-aur] [\-\-dependencies | \-\-no\-dependencies] [\-\-dry\-run] [\-e]
[\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME] [\-\-vcs | \-\-no\-vcs] [\-y]
[\-\-increment | \-\-no\-increment] [\-\-local | \-\-no\-local] [\-\-manual | \-\-no\-manual] [\-u USERNAME]
[\-\-vcs | \-\-no\-vcs] [\-y]
[package ...]
check for packages updates and run build process if requested
@ -586,6 +605,10 @@ just perform check for updates, same as check command
\fB\-e\fR, \fB\-\-exit\-code\fR
return non\-zero exit status if result is empty
.TP
\fB\-\-increment\fR, \fB\-\-no\-increment\fR
increment package release (pkgrel) on duplicate
.TP
\fB\-\-local\fR, \fB\-\-no\-local\fR
enable or disable checking of local packages for updates
@ -671,7 +694,7 @@ key server for key import
usage: ahriman service\-setup [\-h] [\-\-build\-as\-user BUILD_AS_USER] [\-\-build\-command BUILD_COMMAND]
[\-\-from\-configuration FROM_CONFIGURATION] [\-\-generate\-salt | \-\-no\-generate\-salt]
[\-\-makeflags\-jobs | \-\-no\-makeflags\-jobs] [\-\-mirror MIRROR] [\-\-multilib | \-\-no\-multilib]
\-\-packager PACKAGER \-\-repository REPOSITORY [\-\-sign\-key SIGN_KEY]
\-\-packager PACKAGER \-\-repository REPOSITORY [\-\-server SERVER] [\-\-sign\-key SIGN_KEY]
[\-\-sign\-target {disabled,packages,repository}] [\-\-web\-port WEB_PORT]
[\-\-web\-unix\-socket WEB_UNIX_SOCKET]
@ -714,6 +737,10 @@ packager name and email
\fB\-\-repository\fR \fI\,REPOSITORY\/\fR
repository name
.TP
\fB\-\-server\fR \fI\,SERVER\/\fR
server to be used for devtools. If none set, local files will be used
.TP
\fB\-\-sign\-key\fR \fI\,SIGN_KEY\/\fR
sign key id

View File

@ -85,6 +85,7 @@ _shtab_ahriman_options=(
{--report,--no-report}"[force enable or disable reporting to web service (default\: True)]:report:"
{-q,--quiet}"[force disable any logging (default\: False)]"
"--unsafe[allow to run ahriman as non-ahriman user. Some actions might be unavailable (default\: False)]"
"--wait-timeout[wait for lock to be free. Negative value will lead to immediate application run even if there is lock file. In case of zero value, the application will wait infinitely (default\: -1)]:wait_timeout:"
"(- : *)"{-V,--version}"[show program\'s version number and exit]"
)
@ -92,6 +93,7 @@ _shtab_ahriman_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
{-s,--source}"[explicitly specify the package source for this command (default\: PackageSource.Auto)]:source:(auto archive aur directory local remote repository)"
@ -175,6 +177,7 @@ _shtab_ahriman_init_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -191,6 +194,7 @@ _shtab_ahriman_package_add_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
{-s,--source}"[explicitly specify the package source for this command (default\: PackageSource.Auto)]:source:(auto archive aur directory local remote repository)"
@ -227,6 +231,7 @@ _shtab_ahriman_package_update_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) version on duplicate (default\: True)]:increment:"
{-n,--now}"[run update function after (default\: False)]"
"*"{-y,--refresh}"[download fresh package databases from the mirror before actions, -yy to force refresh even if up to date (default\: False)]"
{-s,--source}"[explicitly specify the package source for this command (default\: PackageSource.Auto)]:source:(auto archive aur directory local remote repository)"
@ -265,6 +270,7 @@ _shtab_ahriman_rebuild_options=(
"*--depends-on[only rebuild packages that depend on specified packages (default\: None)]:depends_on:"
"--dry-run[just perform check for packages without rebuild process itself (default\: False)]"
"--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once. (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{-s,--status}"[filter packages by status. Requires --from-database to be set (default\: None)]:status:(unknown pending building failed success)"
{-u,--username}"[build as user (default\: None)]:username:"
@ -342,6 +348,7 @@ _shtab_ahriman_repo_init_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -353,6 +360,7 @@ _shtab_ahriman_repo_rebuild_options=(
"*--depends-on[only rebuild packages that depend on specified packages (default\: None)]:depends_on:"
"--dry-run[just perform check for packages without rebuild process itself (default\: False)]"
"--from-database[read packages from database instead of filesystem. This feature in particular is required in case if you would like to restore repository from another repository instance. Note, however, that in order to restore packages you need to have original ahriman instance run with web service and have run repo-update at least once. (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{-s,--status}"[filter packages by status. Requires --from-database to be set (default\: None)]:status:(unknown pending building failed success)"
{-u,--username}"[build as user (default\: None)]:username:"
@ -384,6 +392,7 @@ _shtab_ahriman_repo_setup_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -406,6 +415,7 @@ _shtab_ahriman_repo_sync_options=(
_shtab_ahriman_repo_tree_options=(
"(- : *)"{-h,--help}"[show this help message and exit]"
{-p,--partitions}"[also divide packages by independent partitions (default\: 1)]:partitions:"
)
_shtab_ahriman_repo_triggers_options=(
@ -419,6 +429,7 @@ _shtab_ahriman_repo_update_options=(
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
{--local,--no-local}"[enable or disable checking of local packages for updates (default\: True)]:local:"
{--manual,--no-manual}"[include or exclude manual updates (default\: True)]:manual:"
{-u,--username}"[build as user (default\: None)]:username:"
@ -475,6 +486,7 @@ _shtab_ahriman_service_setup_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -497,6 +509,7 @@ _shtab_ahriman_setup_options=(
{--multilib,--no-multilib}"[add or do not multilib repository (default\: True)]:multilib:"
"--packager[packager name and email (default\: None)]:packager:"
"--repository[repository name (default\: None)]:repository:"
"--server[server to be used for devtools. If none set, local files will be used (default\: None)]:server:"
"--sign-key[sign key id (default\: None)]:sign_key:"
"*--sign-target[sign options (default\: None)]:sign_target:(disabled packages repository)"
"--web-port[port of the web service (default\: None)]:web_port:"
@ -538,6 +551,7 @@ _shtab_ahriman_update_options=(
{--dependencies,--no-dependencies}"[process missing package dependencies (default\: True)]:dependencies:"
"--dry-run[just perform check for updates, same as check command (default\: False)]"
{-e,--exit-code}"[return non-zero exit status if result is empty (default\: False)]"
{--increment,--no-increment}"[increment package release (pkgrel) on duplicate (default\: True)]:increment:"
{--local,--no-local}"[enable or disable checking of local packages for updates (default\: True)]:local:"
{--manual,--no-manual}"[include or exclude manual updates (default\: True)]:manual:"
{-u,--username}"[build as user (default\: None)]:username:"

84
pyproject.toml Normal file
View File

@ -0,0 +1,84 @@
[build-system]
requires = ["flit_core"]
build-backend = "flit_core.buildapi"
[project]
name = "ahriman"
description = "ArcH linux ReposItory MANager"
readme = "README.md"
requires-python = ">=3.11"
license = {file = "COPYING"}
authors = [
{name = "ahriman team"},
]
dependencies = [
"cerberus",
"inflection",
"passlib",
"requests",
"srcinfo",
]
dynamic = ["version"]
[project.urls]
Documentation = "https://ahriman.readthedocs.io/"
Repository = "https://github.com/arcan1s/ahriman"
Changelog = "https://github.com/arcan1s/ahriman/releases"
[project.scripts]
ahriman = "ahriman.application.ahriman:run"
[project.optional-dependencies]
check = [
"autopep8",
"bandit",
"mypy",
"pylint",
]
docs = [
"Sphinx",
"argparse-manpage",
"pydeps",
"shtab",
"sphinx-argparse",
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734
]
journald = [
"systemd-python",
]
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
pacman = [
"pyalpm",
]
s3 = [
"boto3",
]
tests = [
"pytest",
"pytest-aiohttp",
"pytest-cov",
"pytest-helpers-namespace",
"pytest-mock",
"pytest-resource-path",
"pytest-spec",
]
web = [
"Jinja2",
"aioauth-client",
"aiohttp",
"aiohttp-apispec",
"aiohttp_cors",
"aiohttp_jinja2",
"aiohttp_debugtoolbar",
"aiohttp_session",
"aiohttp_security",
"cryptography",
"requests-unixsocket", # required by unix socket support
]

158
setup.py
View File

@ -1,158 +0,0 @@
from pathlib import Path
from setuptools import find_packages, setup
from typing import Any
metadata_path = Path(__file__).resolve().parent / "src/ahriman/version.py"
metadata: dict[str, Any] = {}
with metadata_path.open() as metadata_file:
exec(metadata_file.read(), metadata) # pylint: disable=exec-used
setup(
name="ahriman",
version=metadata["__version__"],
zip_safe=False,
description="ArcH linux ReposItory MANager",
author="ahriman team",
author_email="",
url="https://github.com/arcan1s/ahriman",
license="GPL3",
packages=find_packages("src"),
package_dir={"": "src"},
package_data={"": ["py.typed"]},
dependency_links=[
],
install_requires=[
"cerberus",
"inflection",
"passlib",
"requests",
"srcinfo",
],
setup_requires=[
],
tests_require=[
"pytest",
"pytest-aiohttp",
"pytest-cov",
"pytest-helpers-namespace",
"pytest-mock",
"pytest-spec",
"pytest-resource-path",
],
include_package_data=True,
scripts=[
"package/bin/ahriman",
],
data_files=[
# configuration
("share/ahriman/settings", [
"package/share/ahriman/settings/ahriman.ini",
]),
("share/ahriman/settings/ahriman.ini.d", [
"package/share/ahriman/settings/ahriman.ini.d/logging.ini",
]),
# systemd files
("lib/systemd/system", [
"package/lib/systemd/system/ahriman@.service",
"package/lib/systemd/system/ahriman@.timer",
"package/lib/systemd/system/ahriman-web@.service",
]),
# templates
("share/ahriman/templates", [
"package/share/ahriman/templates/api.jinja2",
"package/share/ahriman/templates/build-status.jinja2",
"package/share/ahriman/templates/email-index.jinja2",
"package/share/ahriman/templates/error.jinja2",
"package/share/ahriman/templates/repo-index.jinja2",
"package/share/ahriman/templates/shell",
"package/share/ahriman/templates/telegram-index.jinja2",
]),
("share/ahriman/templates/build-status", [
"package/share/ahriman/templates/build-status/alerts.jinja2",
"package/share/ahriman/templates/build-status/key-import-modal.jinja2",
"package/share/ahriman/templates/build-status/login-modal.jinja2",
"package/share/ahriman/templates/build-status/package-add-modal.jinja2",
"package/share/ahriman/templates/build-status/package-info-modal.jinja2",
"package/share/ahriman/templates/build-status/package-rebuild-modal.jinja2",
"package/share/ahriman/templates/build-status/table.jinja2",
]),
("share/ahriman/templates/static", [
"package/share/ahriman/templates/static/favicon.ico",
]),
("share/ahriman/templates/utils", [
"package/share/ahriman/templates/utils/bootstrap-scripts.jinja2",
"package/share/ahriman/templates/utils/style.jinja2",
]),
# man pages
("share/man/man1", [
"docs/ahriman.1",
]),
# shell completions
("share/bash-completion/completions", [
"docs/completions/bash/_ahriman",
]),
("share/zsh/site-functions", [
"docs/completions/zsh/_ahriman",
]),
],
extras_require={
"check": [
"autopep8",
"bandit",
"mypy",
"pylint",
],
"docs": [
"Sphinx",
"argparse-manpage",
"pydeps",
"shtab",
"sphinx-argparse",
"sphinx-rtd-theme>=1.1.1", # https://stackoverflow.com/a/74355734
],
"journald": [
"systemd-python",
],
# FIXME technically this dependency is required, but in some cases we do not have access to
# the libalpm which is required in order to install the package. Thus in case if we do not
# really need to run the application we can move it to "optional" dependencies
"pacman": [
"pyalpm",
],
"s3": [
"boto3",
],
"tests": [
"pytest",
"pytest-aiohttp",
"pytest-cov",
"pytest-helpers-namespace",
"pytest-mock",
"pytest-resource-path",
"pytest-spec",
],
"web": [
"Jinja2",
"aioauth-client",
"aiohttp",
"aiohttp-apispec",
"aiohttp_cors",
"aiohttp_jinja2",
"aiohttp_debugtoolbar",
"aiohttp_session",
"aiohttp_security",
"cryptography",
"requests-unixsocket", # required by unix socket support
],
},
)

View File

@ -17,3 +17,4 @@
# 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.11.0"

View File

@ -19,13 +19,12 @@
#
# pylint: disable=too-many-lines
import argparse
import sys
import tempfile
from pathlib import Path
from typing import TypeVar
from ahriman import version
from ahriman import __version__
from ahriman.application import handlers
from ahriman.core.util import enum_values, extract_user
from ahriman.models.action import Action
@ -85,7 +84,11 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument("-q", "--quiet", help="force disable any logging", action="store_true")
parser.add_argument("--unsafe", help="allow to run ahriman as non-ahriman user. Some actions might be unavailable",
action="store_true")
parser.add_argument("-V", "--version", action="version", version=version.__version__)
parser.add_argument("--wait-timeout", help="wait for lock to be free. Negative value will lead to "
"immediate application run even if there is lock file. "
"In case of zero value, the application will wait infinitely",
type=int, default=-1)
parser.add_argument("-V", "--version", action="version", version=__version__)
subparsers = parser.add_subparsers(title="command", help="command to run", dest="command", required=True)
@ -256,6 +259,8 @@ def _set_package_add_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser.add_argument("--dependencies", help="process missing package dependencies",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) version on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-n", "--now", help="run update function after", action="store_true")
parser.add_argument("-y", "--refresh", help="download fresh package databases from the mirror before actions, "
"-yy to force refresh even if up to date",
@ -577,6 +582,8 @@ def _set_repo_rebuild_parser(root: SubParserAction) -> argparse.ArgumentParser:
"instance. Note, however, that in order to restore packages you need to have original "
"ahriman instance run with web service and have run repo-update at least once.",
action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("-s", "--status", help="filter packages by status. Requires --from-database to be set",
type=BuildStatusEnum, choices=enum_values(BuildStatusEnum))
@ -708,6 +715,8 @@ def _set_repo_tree_parser(root: SubParserAction) -> argparse.ArgumentParser:
parser = root.add_parser("repo-tree", help="dump repository tree",
description="dump repository tree based on packages dependencies",
formatter_class=_formatter)
parser.add_argument("-p", "--partitions", help="also divide packages by independent partitions",
type=int, default=1)
parser.set_defaults(handler=handlers.Structure, lock=None, report=False, quiet=True, unsafe=True)
return parser
@ -751,6 +760,8 @@ def _set_repo_update_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--dry-run", help="just perform check for updates, same as check command", action="store_true")
parser.add_argument("-e", "--exit-code", help="return non-zero exit status if result is empty", action="store_true")
parser.add_argument("--increment", help="increment package release (pkgrel) on duplicate",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--local", help="enable or disable checking of local packages for updates",
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--manual", help="include or exclude manual updates",
@ -884,6 +895,7 @@ def _set_service_setup_parser(root: SubParserAction) -> argparse.ArgumentParser:
action=argparse.BooleanOptionalAction, default=True)
parser.add_argument("--packager", help="packager name and email", required=True)
parser.add_argument("--repository", help="repository name", required=True)
parser.add_argument("--server", help="server to be used for devtools. If none set, local files will be used")
parser.add_argument("--sign-key", help="sign key id")
parser.add_argument("--sign-target", help="sign options", action="append",
type=SignSettings.from_option, choices=enum_values(SignSettings))
@ -994,18 +1006,15 @@ def _set_web_parser(root: SubParserAction) -> argparse.ArgumentParser:
return parser
def run() -> None:
def run() -> int:
"""
run application instance
Returns:
int: application status code
"""
if __name__ == "__main__":
args_parser = _parser()
args = args_parser.parse_args()
args_parser = _parser()
args = args_parser.parse_args()
handler: handlers.Handler = args.handler
status = handler.execute(args)
sys.exit(status)
run()
handler: handlers.Handler = args.handler
return handler.execute(args)

View File

@ -142,8 +142,13 @@ class Application(ApplicationPackages, ApplicationRepository):
while missing := missing_dependencies(with_dependencies.values()):
for package_name, username in missing.items():
package = Package.from_aur(package_name, self.repository.pacman, username)
if (source_dir := self.repository.paths.cache_for(package_name)).is_dir():
# there is local cache, load package from it
package = Package.from_build(source_dir, self.repository.architecture, username)
else:
package = Package.from_aur(package_name, self.repository.pacman, username)
with_dependencies[package.base] = package
# register package in local database
self.database.remote_update(package)
self.repository.reporter.set_unknown(package)

View File

@ -73,6 +73,9 @@ class ApplicationPackages(ApplicationProperties):
Args:
source(str): path to local directory
Raises:
UnknownPackageError: if specified package is unknown or doesn't exist
"""
local_dir = Path(source)
if not local_dir.is_dir():
@ -95,7 +98,7 @@ class ApplicationPackages(ApplicationProperties):
if (source_dir := Path(source)).is_dir():
package = Package.from_build(source_dir, self.architecture, username)
cache_dir = self.repository.paths.cache_for(package.base)
shutil.copytree(source_dir, cache_dir) # copy package to store in caches
shutil.copytree(source_dir, cache_dir, dirs_exist_ok=True) # copy package to store in caches
Sources.init(cache_dir) # we need to run init command in directory where we do have permissions
elif (source_dir := self.repository.paths.cache_for(source)).is_dir():
package = Package.from_build(source_dir, self.architecture, username)
@ -110,8 +113,10 @@ class ApplicationPackages(ApplicationProperties):
Args:
source(str): remote URL of the package archive
Raises:
UnknownPackageError: if specified package is unknown or doesn't exist
"""
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
# timeout=None to suppress pylint warns. Also suppress bandit warnings
try:
response = requests.get(source, stream=True, timeout=None) # nosec
@ -119,6 +124,7 @@ class ApplicationPackages(ApplicationProperties):
except Exception:
raise UnknownPackageError(source)
dst = self.repository.paths.packages / Path(source).name # URL is path, is not it?
with dst.open("wb") as local_file:
for chunk in response.iter_content(chunk_size=1024):
local_file.write(chunk)
@ -145,7 +151,7 @@ class ApplicationPackages(ApplicationProperties):
username(str | None, optional): optional override of username for build process (Default value = None)
"""
for name in names:
resolved_source = source.resolve(name)
resolved_source = source.resolve(name, self.repository.paths)
fn = getattr(self, f"_add_{resolved_source.value}")
fn(name, username)

View File

@ -123,7 +123,8 @@ class ApplicationRepository(ApplicationProperties):
result.extend(unknown_aur(package)) # local package not found
return result
def update(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
def update(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
bump_pkgrel: bool = False) -> Result:
"""
run package updates
@ -131,6 +132,7 @@ class ApplicationRepository(ApplicationProperties):
updates(Iterable[Package]): list of packages to update
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
Returns:
Result: update result
@ -150,7 +152,7 @@ class ApplicationRepository(ApplicationProperties):
tree = Tree.resolve(updates)
for num, level in enumerate(tree):
self.logger.info("processing level #%i %s", num, [package.base for package in level])
build_result = self.repository.process_build(level, packagers)
build_result = self.repository.process_build(level, packagers, bump_pkgrel=bump_pkgrel)
packages = self.repository.packages_built()
process_update(packages, build_result)

View File

@ -52,5 +52,5 @@ class Add(Handler):
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Add.check_if_empty(args.exit_code, result.is_empty)

View File

@ -61,7 +61,7 @@ class Handler:
list[str]: list of architectures for which tree is created
Raises:
MissingArchitecture: if no architecture set and automatic detection is not allowed or failed
MissingArchitectureError: if no architecture set and automatic detection is not allowed or failed
"""
if not cls.ALLOW_AUTO_ARCHITECTURE_RUN and args.architecture is None:
# for some parsers (e.g. config) we need to run with specific architecture
@ -94,10 +94,13 @@ class Handler:
"""
try:
configuration = Configuration.from_path(args.configuration, architecture)
log_handler = Log.handler(args.log_handler)
Log.load(configuration, log_handler, quiet=args.quiet, report=args.report)
with Lock(args, architecture, configuration):
cls.run(args, architecture, configuration, report=args.report)
return True
except ExitCode:
return False
@ -118,7 +121,7 @@ class Handler:
int: 0 on success, 1 otherwise
Raises:
MultipleArchitectures: if more than one architecture supplied and no multi architecture supported
MultipleArchitecturesError: if more than one architecture supplied and no multi architecture supported
"""
architectures = cls.architectures_extract(args)
@ -128,8 +131,7 @@ class Handler:
raise MultipleArchitecturesError(args.command)
with Pool(len(architectures)) as pool:
result = pool.starmap(
cls.call, [(args, architecture) for architecture in architectures])
result = pool.starmap(cls.call, [(args, architecture) for architecture in architectures])
else:
result = [cls.call(args, architectures.pop())]
@ -164,4 +166,4 @@ class Handler:
ExitCode: if result is empty and check is enabled
"""
if enabled and predicate:
raise ExitCode()
raise ExitCode

View File

@ -51,16 +51,17 @@ class Patch(Handler):
application = Application(architecture, configuration, report=report)
application.on_start()
if args.action == Action.Update and args.variable is not None:
patch = Patch.patch_create_from_function(args.variable, args.patch)
Patch.patch_set_create(application, args.package, patch)
elif args.action == Action.Update and args.variable is None:
package_base, patch = Patch.patch_create_from_diff(args.package, architecture, args.track)
Patch.patch_set_create(application, package_base, patch)
elif args.action == Action.List:
Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
elif args.action == Action.Remove:
Patch.patch_set_remove(application, args.package, args.variable)
match args.action:
case Action.Update if args.variable is not None:
patch = Patch.patch_create_from_function(args.variable, args.patch)
Patch.patch_set_create(application, args.package, patch)
case Action.Update:
package_base, patch = Patch.patch_create_from_diff(args.package, architecture, args.track)
Patch.patch_set_create(application, package_base, patch)
case Action.List:
Patch.patch_set_list(application, args.package, args.variable, args.exit_code)
case Action.Remove:
Patch.patch_set_remove(application, args.package, args.variable)
@staticmethod
def patch_create_from_diff(sources_dir: Path, architecture: str, track: list[str]) -> tuple[str, PkgbuildPatch]:

View File

@ -53,7 +53,7 @@ class Rebuild(Handler):
application.print_updates(updates, log_fn=print)
return
result = application.update(updates, args.username)
result = application.update(updates, args.username, bump_pkgrel=args.increment)
Rebuild.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -81,7 +81,7 @@ class Search(Handler):
list[AURPackage]: sorted list for packages
Raises:
InvalidOption: if search fields is not in list of allowed ones
OptionError: if search fields is not in list of allowed ones
"""
if sort_by not in Search.SORT_FIELDS:
raise OptionError(sort_by)

View File

@ -19,7 +19,7 @@
#
import argparse
from ahriman import version
from ahriman import __version__
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
@ -48,8 +48,8 @@ class ServiceUpdates(Handler):
application = Application(architecture, configuration, report=report)
remote = Package.from_aur("ahriman", application.repository.pacman, None)
release = remote.version.rsplit("-", 1)[-1] # we don't store pkgrel locally, so we just append it
local_version = f"{version.__version__}-{release}"
_, release = remote.version.rsplit("-", 1) # we don't store pkgrel locally, so we just append it
local_version = f"{__version__}-{release}"
# technically we would like to compare versions, but it is fine to raise an exception in case if locally
# installed package is newer than in AUR

View File

@ -63,8 +63,9 @@ class Setup(Handler):
Setup.configuration_create_makepkg(args.packager, args.makeflags_jobs, application.repository.paths)
Setup.executable_create(application.repository.paths, args.build_command, architecture)
repository_server = f"file://{application.repository.paths.repository}" if args.server is None else args.server
Setup.configuration_create_devtools(args.build_command, architecture, args.from_configuration, args.mirror,
args.multilib, args.repository, application.repository.paths)
args.multilib, args.repository, repository_server)
Setup.configuration_create_sudo(application.repository.paths, args.build_command, architecture)
application.repository.repo.init()
@ -134,7 +135,7 @@ class Setup(Handler):
@staticmethod
def configuration_create_devtools(prefix: str, architecture: str, source: Path, mirror: str | None,
multilib: bool, repository: str, paths: RepositoryPaths) -> None:
multilib: bool, repository: str, repository_server: str) -> None:
"""
create configuration for devtools based on ``source`` configuration
@ -148,7 +149,7 @@ class Setup(Handler):
mirror(str | None): link to package server mirror
multilib(bool): add or do not multilib repository to the configuration
repository(str): repository name
paths(RepositoryPaths): repository paths instance
repository_server(str): url of the repository
"""
# allow_no_value=True is required because pacman uses boolean configuration in which just keys present
# (e.g. NoProgressBar) which will lead to exception
@ -178,7 +179,7 @@ class Setup(Handler):
# add repository itself
configuration.set_option(repository, "SigLevel", "Never") # we don't care
configuration.set_option(repository, "Server", f"file://{paths.repository}")
configuration.set_option(repository, "Server", repository_server)
target = source.parent / f"{prefix}-{architecture}.conf"
with target.open("w") as devtools_configuration:

View File

@ -50,14 +50,14 @@ class Status(Handler):
# we are using reporter here
client = Application(architecture, configuration, report=True).repository.reporter
if args.ahriman:
service_status = client.get_internal()
service_status = client.status_get()
StatusPrinter(service_status.status).print(verbose=args.info)
if args.package:
packages: list[tuple[Package, BuildStatus]] = sum(
(client.get(base) for base in args.package),
(client.package_get(base) for base in args.package),
start=[])
else:
packages = client.get(None)
packages = client.package_get(None)
Status.check_if_empty(args.exit_code, not packages)

View File

@ -46,13 +46,14 @@ class StatusUpdate(Handler):
# we are using reporter here
client = Application(architecture, configuration, report=True).repository.reporter
if args.action == Action.Update and args.package:
# update packages statuses
for package in args.package:
client.update(package, args.status)
elif args.action == Action.Update:
# update service status
client.update_self(args.status)
elif args.action == Action.Remove:
for package in args.package:
client.remove(package)
match args.action:
case Action.Update if args.package:
# update packages statuses
for package in args.package:
client.package_update(package, args.status)
case Action.Update:
# update service status
client.status_update(args.status)
case Action.Remove:
for package in args.package:
client.package_remove(package)

View File

@ -22,7 +22,7 @@ import argparse
from ahriman.application.application import Application
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import TreePrinter
from ahriman.core.formatters import StringPrinter, TreePrinter
from ahriman.core.tree import Tree
@ -45,8 +45,14 @@ class Structure(Handler):
report(bool): force enable or disable reporting
"""
application = Application(architecture, configuration, report=report)
packages = application.repository.packages()
partitions = Tree.partition(application.repository.packages(), count=args.partitions)
tree = Tree.resolve(packages)
for num, level in enumerate(tree):
TreePrinter(num, level).print(verbose=True, separator=" ")
for partition_id, partition in enumerate(partitions):
StringPrinter(f"partition #{partition_id}").print(verbose=False)
tree = Tree.resolve(partition)
for num, level in enumerate(tree):
TreePrinter(num, level).print(verbose=True, separator=" ")
# empty line
StringPrinter("").print(verbose=False)

View File

@ -54,7 +54,7 @@ class Update(Handler):
packagers = Packagers(args.username, {package.base: package.packager for package in packages})
application.print_updates(packages, log_fn=application.logger.info)
result = application.update(packages, packagers)
result = application.update(packages, packagers, bump_pkgrel=args.increment)
Update.check_if_empty(args.exit_code, result.is_empty)
@staticmethod

View File

@ -49,18 +49,19 @@ class Users(Handler):
"""
database = SQLite.load(configuration)
if args.action == Action.Update:
user = Users.user_create(args)
# if password is left blank we are not going to require salt to be set
salt = configuration.get("auth", "salt") if user.password else ""
database.user_update(user.hash_password(salt))
elif args.action == Action.List:
users = database.user_list(args.username, args.role)
Users.check_if_empty(args.exit_code, not users)
for user in users:
UserPrinter(user).print(verbose=True)
elif args.action == Action.Remove:
database.user_remove(args.username)
match args.action:
case Action.Update:
user = Users.user_create(args)
# if password is left blank we are not going to require salt to be set
salt = configuration.get("auth", "salt", fallback="") if user.password else ""
database.user_update(user.hash_password(salt))
case Action.List:
users = database.user_list(args.username, args.role)
Users.check_if_empty(args.exit_code, not users)
for user in users:
UserPrinter(user).print(verbose=True)
case Action.Remove:
database.user_remove(args.username)
@staticmethod
def user_create(args: argparse.Namespace) -> User:
@ -72,6 +73,9 @@ class Users(Handler):
Returns:
User: built user descriptor
Raises:
PasswordError: password input is invalid
"""
def read_password() -> str:
first_password = getpass.getpass()

View File

@ -24,7 +24,7 @@ import sys
from collections.abc import Generator
from importlib import metadata
from ahriman import version
from ahriman import __version__
from ahriman.application.handlers import Handler
from ahriman.core.configuration import Configuration
from ahriman.core.formatters import VersionPrinter
@ -52,7 +52,7 @@ class Versions(Handler):
configuration(Configuration): configuration instance
report(bool): force enable or disable reporting
"""
VersionPrinter(f"Module version {version.__version__}",
VersionPrinter(f"Module version {__version__}",
{"Python": sys.version}).print(verbose=False, separator=" ")
packages = Versions.package_dependencies("ahriman")
VersionPrinter("Installed packages", dict(packages)).print(verbose=False, separator=" ")

View File

@ -33,7 +33,6 @@ class Web(Handler):
ALLOW_AUTO_ARCHITECTURE_RUN = False
ALLOW_MULTI_ARCHITECTURE_RUN = False # required to be able to spawn external processes
COMMAND_ARGS_WHITELIST = ["force", "log_handler", ""]
@classmethod
def run(cls, args: argparse.Namespace, architecture: str, configuration: Configuration, *, report: bool) -> None:
@ -89,3 +88,7 @@ class Web(Handler):
yield "--quiet"
if args.unsafe:
yield "--unsafe"
# arguments from configuration
if (wait_timeout := configuration.getint("web", "wait_timeout", fallback=None)) is not None:
yield from ["--wait-timeout", str(wait_timeout)]

View File

@ -19,16 +19,18 @@
#
import argparse
from pathlib import Path
from types import TracebackType
from typing import Literal, Self
from ahriman import version
from ahriman import __version__
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRunError
from ahriman.core.log import LazyLogging
from ahriman.core.status.client import Client
from ahriman.core.util import check_user
from ahriman.models.build_status import BuildStatusEnum
from ahriman.models.waiter import Waiter
class Lock(LazyLogging):
@ -41,6 +43,7 @@ class Lock(LazyLogging):
reporter(Client): build status reporter instance
paths(RepositoryPaths): repository paths instance
unsafe(bool): skip user check
wait_timeout(int): wait in seconds until lock will free
Examples:
Instance of this class except for controlling file-based lock is also required for basic applications checks.
@ -65,9 +68,11 @@ class Lock(LazyLogging):
architecture(str): repository architecture
configuration(Configuration): configuration instance
"""
self.path = args.lock.with_stem(f"{args.lock.stem}_{architecture}") if args.lock is not None else None
self.force = args.force
self.unsafe = args.unsafe
self.path: Path | None = \
args.lock.with_stem(f"{args.lock.stem}_{architecture}") if args.lock is not None else None
self.force: bool = args.force
self.unsafe: bool = args.unsafe
self.wait_timeout: int = args.wait_timeout
self.paths = configuration.repository_paths
self.reporter = Client.load(configuration, report=args.report)
@ -76,10 +81,10 @@ class Lock(LazyLogging):
"""
check web server version
"""
status = self.reporter.get_internal()
if status.version is not None and status.version != version.__version__:
status = self.reporter.status_get()
if status.version is not None and status.version != __version__:
self.logger.warning("status watcher version mismatch, our %s, their %s",
version.__version__, status.version)
__version__, status.version)
def check_user(self) -> None:
"""
@ -101,14 +106,27 @@ class Lock(LazyLogging):
create lock file
Raises:
DuplicateRun: if lock exists and no force flag supplied
DuplicateRunError: if lock exists and no force flag supplied
"""
if self.path is None:
return
try:
self.path.touch(exist_ok=self.force)
except FileExistsError:
raise DuplicateRunError()
raise DuplicateRunError from None
def watch(self) -> None:
"""
watch until lock disappear
"""
# there are reasons why we are not using inotify here. First of all, if we would use it, it would bring to
# race conditions because multiple processes will be notified in the same time. Secondly, it is good library,
# but platform-specific, and we only need to check if file exists
if self.path is None:
return
waiter = Waiter(self.wait_timeout)
waiter.wait(self.path.is_file)
def __enter__(self) -> Self:
"""
@ -117,16 +135,18 @@ class Lock(LazyLogging):
1. Check user UID
2. Check if there is lock file
3. Check web status watcher status
4. Create lock file and directory tree
5. Report to status page if enabled
4. Wait for lock file to be free
5. Create lock file and directory tree
6. Report to status page if enabled
Returns:
Self: always instance of self
"""
self.check_user()
self.check_version()
self.watch()
self.create()
self.reporter.update_self(BuildStatusEnum.Building)
self.reporter.status_update(BuildStatusEnum.Building)
return self
def __exit__(self, exc_type: type[Exception] | None, exc_val: Exception | None,
@ -144,5 +164,5 @@ class Lock(LazyLogging):
"""
self.clear()
status = BuildStatusEnum.Success if exc_val is None else BuildStatusEnum.Failed
self.reporter.update_self(status)
self.reporter.status_update(status)
return False

View File

@ -49,6 +49,16 @@ class Pacman(LazyLogging):
self.__create_handle_fn: Callable[[], Handle] = lambda: self.__create_handle(
architecture, configuration, refresh_database=refresh_database)
@cached_property
def handle(self) -> Handle:
"""
pyalpm handle
Returns:
Handle: generated pyalpm handle instance
"""
return self.__create_handle_fn()
def __create_handle(self, architecture: str, configuration: Configuration, *,
refresh_database: PacmanSynchronization) -> Handle:
"""
@ -79,16 +89,6 @@ class Pacman(LazyLogging):
return handle
@cached_property
def handle(self) -> Handle:
"""
pyalpm handle
Returns:
Handle: generated pyalpm handle instance
"""
return self.__create_handle_fn()
def database_copy(self, handle: Handle, database: DB, pacman_root: Path, paths: RepositoryPaths, *,
use_ahriman_cache: bool) -> None:
"""
@ -116,7 +116,7 @@ class Pacman(LazyLogging):
src = repository_database(pacman_root)
if not src.is_file():
self.logger.warning("repository %s is set to be used, however, no working copy was found", database.name)
return # database for some reasons deos not exist
return # database for some reason deos not exist
self.logger.info("copy pacman database from operating system root to ahriman's home")
shutil.copy(src, dst)
paths.chown(dst)

View File

@ -17,14 +17,11 @@
# 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 requests
from typing import Any
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import Remote
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage
@ -36,13 +33,11 @@ class AUR(Remote):
DEFAULT_AUR_URL(str): (class attribute) default AUR url
DEFAULT_RPC_URL(str): (class attribute) default AUR RPC url
DEFAULT_RPC_VERSION(str): (class attribute) default AUR RPC version
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
"""
DEFAULT_AUR_URL = "https://aur.archlinux.org"
DEFAULT_RPC_URL = f"{DEFAULT_AUR_URL}/rpc"
DEFAULT_RPC_VERSION = "5"
DEFAULT_TIMEOUT = 30
@classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str:
@ -83,7 +78,7 @@ class AUR(Remote):
list[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
PackageInfoError: for error API response
"""
response_type = response["type"]
if response_type == "error":
@ -91,7 +86,7 @@ class AUR(Remote):
raise PackageInfoError(error_details)
return [AURPackage.from_json(package) for package in response["results"]]
def make_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]:
def aur_request(self, request_type: str, *args: str, **kwargs: str) -> list[AURPackage]:
"""
perform request to AUR RPC
@ -103,34 +98,20 @@ class AUR(Remote):
Returns:
list[AURPackage]: response parsed to package list
"""
query: dict[str, Any] = {
"type": request_type,
"v": self.DEFAULT_RPC_VERSION
}
query: list[tuple[str, str]] = [
("type", request_type),
("v", self.DEFAULT_RPC_VERSION),
]
arg_query = "arg[]" if len(args) > 1 else "arg"
query[arg_query] = list(args)
for arg in args:
query.append((arg_query, arg))
for key, value in kwargs.items():
query[key] = value
query.append((key, value))
try:
response = requests.get(
self.DEFAULT_RPC_URL,
params=query,
headers={"User-Agent": self.DEFAULT_USER_AGENT},
timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:
self.logger.exception(
"could not perform request by using type %s: %s",
request_type,
exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request by using type %s", request_type)
raise
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
"""
@ -142,12 +123,15 @@ class AUR(Remote):
Returns:
AURPackage: package which match the package name
Raises:
UnknownPackageError: package doesn't exist
"""
packages = self.make_request("info", package_name)
packages = self.aur_request("info", package_name)
try:
return next(package for package in packages if package.name == package_name)
except StopIteration:
raise UnknownPackageError(package_name)
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
"""
@ -160,4 +144,4 @@ class AUR(Remote):
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return self.make_request("search", *keywords, by="name-desc")
return self.aur_request("search", *keywords, by="name-desc")

View File

@ -17,14 +17,11 @@
# 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 requests
from typing import Any
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import Remote
from ahriman.core.exceptions import PackageInfoError, UnknownPackageError
from ahriman.core.util import exception_response_text
from ahriman.models.aur_package import AURPackage
@ -37,14 +34,12 @@ class Official(Remote):
DEFAULT_ARCHLINUX_GIT_URL(str): (class attribute) default url for git packages
DEFAULT_SEARCH_REPOSITORIES(list[str]): (class attribute) default list of repositories to search
DEFAULT_RPC_URL(str): (class attribute) default archlinux repositories RPC url
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
"""
DEFAULT_ARCHLINUX_GIT_URL = "https://gitlab.archlinux.org"
DEFAULT_ARCHLINUX_URL = "https://archlinux.org"
DEFAULT_SEARCH_REPOSITORIES = ["Core", "Extra", "Multilib"]
DEFAULT_RPC_URL = "https://archlinux.org/packages/search/json"
DEFAULT_TIMEOUT = 30
@classmethod
def remote_git_url(cls, package_base: str, repository: str) -> str:
@ -85,13 +80,13 @@ class Official(Remote):
list[AURPackage]: list of parsed packages
Raises:
InvalidPackageInfo: for error API response
PackageInfoError: for error API response
"""
if not response["valid"]:
raise PackageInfoError("API validation error")
return [AURPackage.from_repo(package) for package in response["results"]]
def make_request(self, *args: str, by: str) -> list[AURPackage]:
def arch_request(self, *args: str, by: str) -> list[AURPackage]:
"""
perform request to official repositories RPC
@ -102,20 +97,15 @@ class Official(Remote):
Returns:
list[AURPackage]: response parsed to package list
"""
try:
response = requests.get(
self.DEFAULT_RPC_URL,
params={by: args, "repo": self.DEFAULT_SEARCH_REPOSITORIES},
headers={"User-Agent": self.DEFAULT_USER_AGENT},
timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
return self.parse_response(response.json())
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
query: list[tuple[str, str]] = [
("repo", repository)
for repository in self.DEFAULT_SEARCH_REPOSITORIES
]
for arg in args:
query.append((by, arg))
response = self.make_request("GET", self.DEFAULT_RPC_URL, params=query)
return self.parse_response(response.json())
def package_info(self, package_name: str, *, pacman: Pacman) -> AURPackage:
"""
@ -127,12 +117,15 @@ class Official(Remote):
Returns:
AURPackage: package which match the package name
Raises:
UnknownPackageError: package doesn't exist
"""
packages = self.make_request(package_name, by="name")
packages = self.arch_request(package_name, by="name")
try:
return next(package for package in packages if package.name == package_name)
except StopIteration:
raise UnknownPackageError(package_name)
raise UnknownPackageError(package_name) from None
def package_search(self, *keywords: str, pacman: Pacman) -> list[AURPackage]:
"""
@ -145,4 +138,4 @@ class Official(Remote):
Returns:
list[AURPackage]: list of packages which match the criteria
"""
return self.make_request(*keywords, by="q")
return self.arch_request(*keywords, by="q")

View File

@ -48,8 +48,11 @@ class OfficialSyncdb(Official):
Returns:
AURPackage: package which match the package name
Raises:
UnknownPackageError: package doesn't exist
"""
try:
return next(AURPackage.from_pacman(package) for package in pacman.package_get(package_name))
except StopIteration:
raise UnknownPackageError(package_name)
raise UnknownPackageError(package_name) from None

View File

@ -17,19 +17,15 @@
# 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.core.alpm.pacman import Pacman
from ahriman.core.log import LazyLogging
from ahriman.core.http import SyncHttpClient
from ahriman.models.aur_package import AURPackage
class Remote(LazyLogging):
class Remote(SyncHttpClient):
"""
base class for remote package search
Attributes:
DEFAULT_USER_AGENT(str): (class attribute) default user agent
Examples:
These classes are designed to be used without instancing. In order to achieve it several class methods are
provided: ``info``, ``multisearch`` and ``search``. Thus, the basic flow is the following::
@ -43,8 +39,6 @@ class Remote(LazyLogging):
directly, whereas ``multisearch`` splits search one by one and finds intersection between search results.
"""
DEFAULT_USER_AGENT = f"ahriman/{version.__version__}"
@classmethod
def info(cls, package_name: str, *, pacman: Pacman) -> AURPackage:
"""

View File

@ -71,7 +71,7 @@ class Repo(LazyLogging):
"""
Repo._check_output(
"repo-add", *self.sign_args, "-R", str(self.repo_path), str(path),
exception=BuildError(path.name),
exception=BuildError.from_process(path.name),
cwd=self.paths.repository,
logger=self.logger,
user=self.uid)
@ -98,7 +98,7 @@ class Repo(LazyLogging):
# remove package from registry
Repo._check_output(
"repo-remove", *self.sign_args, str(self.repo_path), package,
exception=BuildError(package),
exception=BuildError.from_process(package),
cwd=self.paths.repository,
logger=self.logger,
user=self.uid)

View File

@ -74,14 +74,15 @@ class Auth(LazyLogging):
Returns:
Auth: authorization module according to current settings
"""
provider = AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled"))
if provider == AuthSettings.Configuration:
from ahriman.core.auth.mapping import Mapping
return Mapping(configuration, database)
if provider == AuthSettings.OAuth:
from ahriman.core.auth.oauth import OAuth
return OAuth(configuration, database)
return Auth(configuration)
match AuthSettings.from_option(configuration.get("auth", "target", fallback="disabled")):
case AuthSettings.Configuration:
from ahriman.core.auth.mapping import Mapping
return Mapping(configuration, database)
case AuthSettings.OAuth:
from ahriman.core.auth.oauth import OAuth
return OAuth(configuration, database)
case _:
return Auth(configuration)
async def check_credentials(self, username: str | None, password: str | None) -> bool:
"""

View File

@ -46,7 +46,7 @@ class Mapping(Auth):
"""
Auth.__init__(self, configuration, provider)
self.database = database
self.salt = configuration.get("auth", "salt")
self.salt = configuration.get("auth", "salt", fallback="")
async def check_credentials(self, username: str | None, password: str | None) -> bool:
"""

View File

@ -28,8 +28,8 @@ from ahriman.models.auth_settings import AuthSettings
class OAuth(Mapping):
"""
OAuth's user authorization.
It is required to create application first and put application credentials.
User authorization implementation via OAuth. It is required to create application first and put application
credentials.
Attributes:
client_id(str): application client id
@ -81,7 +81,7 @@ class OAuth(Mapping):
type[aioauth_client.OAuth2Client]: loaded provider type
Raises:
InvalidOption: in case if invalid OAuth provider name supplied
OptionError: in case if invalid OAuth provider name supplied
"""
provider: type[aioauth_client.OAuth2Client] = getattr(aioauth_client, name)
try:

View File

@ -36,9 +36,11 @@ class Sources(LazyLogging):
Attributes:
DEFAULT_BRANCH(str): (class attribute) default branch to process git repositories.
Must be used only for local stored repositories, use RemoteSource descriptor instead for real packages
DEFAULT_COMMIT_AUTHOR(tuple[str, str]): (class attribute) default commit author to be used if none set
"""
DEFAULT_BRANCH = "master" # default fallback branch
DEFAULT_COMMIT_AUTHOR = ("ahriman", "ahriman@localhost")
_check_output = check_output
@ -61,13 +63,13 @@ class Sources(LazyLogging):
return [PkgbuildPatch("arch", list(architectures))]
@staticmethod
def fetch(sources_dir: Path, remote: RemoteSource | None) -> None:
def fetch(sources_dir: Path, remote: RemoteSource) -> None:
"""
either clone repository or update it to origin/``remote.branch``
Args:
sources_dir(Path): local path to fetch
remote(RemoteSource | None): remote target (from where to fetch)
remote(RemoteSource): remote target (from where to fetch)
"""
instance = Sources()
# local directory exists and there is .git directory
@ -77,13 +79,14 @@ class Sources(LazyLogging):
instance.logger.info("skip update at %s because there are no branches configured", sources_dir)
return
branch = remote.branch if remote is not None else instance.DEFAULT_BRANCH
branch = remote.branch or instance.DEFAULT_BRANCH
if is_initialized_git:
instance.logger.info("update HEAD to remote at %s using branch %s", sources_dir, branch)
Sources._check_output("git", "fetch", "origin", branch, cwd=sources_dir, logger=instance.logger)
elif remote is not None:
Sources._check_output("git", "fetch", "--quiet", "origin", branch,
cwd=sources_dir, logger=instance.logger)
elif remote.git_url is not None:
instance.logger.info("clone remote %s to %s using branch %s", remote.git_url, sources_dir, branch)
Sources._check_output("git", "clone", "--branch", branch, "--single-branch",
Sources._check_output("git", "clone", "--quiet", "--branch", branch, "--single-branch",
remote.git_url, str(sources_dir), cwd=sources_dir.parent, logger=instance.logger)
else:
# it will cause an exception later
@ -91,11 +94,12 @@ class Sources(LazyLogging):
# and now force reset to our branch
Sources._check_output("git", "checkout", "--force", branch, cwd=sources_dir, logger=instance.logger)
Sources._check_output("git", "reset", "--hard", f"origin/{branch}", cwd=sources_dir, logger=instance.logger)
Sources._check_output("git", "reset", "--quiet", "--hard", f"origin/{branch}",
cwd=sources_dir, logger=instance.logger)
# move content if required
# we are using full path to source directory in order to make append possible
pkgbuild_dir = remote.pkgbuild_dir if remote is not None else sources_dir.resolve()
pkgbuild_dir = remote.pkgbuild_dir or sources_dir.resolve()
instance.move((sources_dir / pkgbuild_dir).resolve(), sources_dir)
@staticmethod
@ -122,14 +126,16 @@ class Sources(LazyLogging):
sources_dir(Path): local path to sources
"""
instance = Sources()
Sources._check_output("git", "init", "--initial-branch", instance.DEFAULT_BRANCH,
cwd=sources_dir, logger=instance.logger)
if not (sources_dir / ".git").is_dir():
# skip initializing in case if it was already
Sources._check_output("git", "init", "--quiet", "--initial-branch", instance.DEFAULT_BRANCH,
cwd=sources_dir, logger=instance.logger)
# extract local files...
files = ["PKGBUILD", ".SRCINFO"] + [str(path) for path in Package.local_files(sources_dir)]
instance.add(sources_dir, *files)
# ...and commit them
instance.commit(sources_dir, author="ahriman <ahriman@localhost>")
instance.commit(sources_dir)
@staticmethod
def load(sources_dir: Path, package: Package, patches: list[PkgbuildPatch], paths: RepositoryPaths) -> None:
@ -170,7 +176,8 @@ class Sources(LazyLogging):
return f"{diff}\n" # otherwise, patch will be broken
@staticmethod
def push(sources_dir: Path, remote: RemoteSource, *pattern: str, commit_author: str | None = None) -> None:
def push(sources_dir: Path, remote: RemoteSource, *pattern: str,
commit_author: tuple[str, str] | None = None) -> None:
"""
commit selected changes and push files to the remote repository
@ -178,13 +185,15 @@ class Sources(LazyLogging):
sources_dir(Path): local path to git repository
remote(RemoteSource): remote target, branch and url
*pattern(str): glob patterns
commit_author(str | None, optional): commit author in form of git config (i.e. ``user <user@host>``)
(Default value = None)
commit_author(tuple[str, str] | None, optional): commit author if any (Default value = None)
"""
instance = Sources()
instance.add(sources_dir, *pattern)
instance.commit(sources_dir, author=commit_author)
Sources._check_output("git", "push", remote.git_url, remote.branch, cwd=sources_dir, logger=instance.logger)
if not instance.commit(sources_dir, commit_author=commit_author):
return # no changes to push, just skip action
git_url, branch = remote.git_source()
Sources._check_output("git", "push", "--quiet", git_url, branch, cwd=sources_dir, logger=instance.logger)
def add(self, sources_dir: Path, *pattern: str, intent_to_add: bool = False) -> None:
"""
@ -208,7 +217,8 @@ class Sources(LazyLogging):
Sources._check_output("git", "add", *args, *[str(fn.relative_to(sources_dir)) for fn in found_files],
cwd=sources_dir, logger=self.logger)
def commit(self, sources_dir: Path, message: str | None = None, author: str | None = None) -> None:
def commit(self, sources_dir: Path, message: str | None = None,
commit_author: tuple[str, str] | None = None) -> bool:
"""
commit changes
@ -216,14 +226,29 @@ class Sources(LazyLogging):
sources_dir(Path): local path to git repository
message(str | None, optional): optional commit message if any. If none set, message will be generated
according to the current timestamp (Default value = None)
author(str | None, optional): optional commit author if any (Default value = None)
commit_author(tuple[str, str] | None, optional): optional commit author if any (Default value = None)
Returns:
bool: True in case if changes have been committed and False otherwise
"""
if not self.has_changes(sources_dir):
return False # nothing to commit
if message is None:
message = f"Autogenerated commit at {utcnow()}"
args = ["--allow-empty", "--message", message]
if author is not None:
args.extend(["--author", author])
Sources._check_output("git", "commit", *args, cwd=sources_dir, logger=self.logger)
args = ["--message", message]
environment: dict[str, str] = {}
if commit_author is None:
commit_author = self.DEFAULT_COMMIT_AUTHOR
user, email = commit_author
environment["GIT_AUTHOR_NAME"] = environment["GIT_COMMITTER_NAME"] = user
environment["GIT_AUTHOR_EMAIL"] = environment["GIT_COMMITTER_EMAIL"] = email
Sources._check_output("git", "commit", "--quiet", *args,
cwd=sources_dir, logger=self.logger, environment=environment)
return True
def diff(self, sources_dir: Path) -> str:
"""
@ -237,6 +262,20 @@ class Sources(LazyLogging):
"""
return Sources._check_output("git", "diff", cwd=sources_dir, logger=self.logger)
def has_changes(self, sources_dir: Path) -> bool:
"""
check if there are changes in current git tree
Args:
sources_dir(Path): local path to git repository
Returns:
bool: True if there are uncommitted changes and False otherwise
"""
# there is --exit-code argument to diff, however, there might be other process errors
changes = Sources._check_output("git", "diff", "--cached", "--name-only", cwd=sources_dir, logger=self.logger)
return bool(changes)
def move(self, pkgbuild_dir: Path, sources_dir: Path) -> None:
"""
move content from pkgbuild_dir to sources_dir

View File

@ -26,6 +26,7 @@ from ahriman.core.exceptions import BuildError
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output
from ahriman.models.package import Package
from ahriman.models.pkgbuild_patch import PkgbuildPatch
from ahriman.models.repository_paths import RepositoryPaths
@ -34,6 +35,11 @@ class Task(LazyLogging):
base package build task
Attributes:
archbuild_flags(list[str]): command flags for archbuild command
architecture(str): repository architecture
build_command(str): build command
makechroootpkg_flags(list[str]): command flags for makechrootpkg command
makepkg_flags(list[str]): command flags for makepkg command
package(Package): package definitions
paths(RepositoryPaths): repository paths instance
uid(int): uid of the repository owner user
@ -41,18 +47,21 @@ class Task(LazyLogging):
_check_output = check_output
def __init__(self, package: Package, configuration: Configuration, paths: RepositoryPaths) -> None:
def __init__(self, package: Package, configuration: Configuration, architecture: str,
paths: RepositoryPaths) -> None:
"""
default constructor
Args:
package(Package): package definitions
configuration(Configuration): configuration instance
architecture(str): repository architecture
paths(RepositoryPaths): repository paths instance
"""
self.package = package
self.paths = paths
self.uid, _ = paths.root_owner
self.architecture = architecture
self.archbuild_flags = configuration.getlist("build", "archbuild_flags", fallback=[])
self.build_command = configuration.get("build", "build_command")
@ -83,7 +92,7 @@ class Task(LazyLogging):
Task._check_output(
*command,
exception=BuildError(self.package.base),
exception=BuildError.from_process(self.package.base),
cwd=sources_dir,
logger=self.logger,
user=self.uid,
@ -92,18 +101,29 @@ class Task(LazyLogging):
# well it is not actually correct, but we can deal with it
packages = Task._check_output(
"makepkg", "--packagelist",
exception=BuildError(self.package.base),
exception=BuildError.from_process(self.package.base),
cwd=sources_dir,
logger=self.logger
).splitlines()
return [Path(package) for package in packages]
def init(self, sources_dir: Path, database: SQLite) -> None:
def init(self, sources_dir: Path, database: SQLite, local_version: str | None) -> None:
"""
fetch package from git
Args:
sources_dir(Path): local path to fetch
database(SQLite): database instance
local_version(str | None): local version of the package. If set and equal to current version, it will
automatically bump pkgrel
"""
Sources.load(sources_dir, self.package, database.patches_get(self.package.base), self.paths)
if local_version is None:
return # there is no local package or pkgrel increment is disabled
# load fresh package
loaded_package = Package.from_build(sources_dir, self.architecture, None)
if (pkgrel := loaded_package.next_pkgrel(local_version)) is not None:
self.logger.info("package %s is the same as in repo, bumping pkgrel to %s", self.package.base, pkgrel)
patch = PkgbuildPatch("pkgrel", pkgrel)
patch.write(sources_dir / "PKGBUILD")

View File

@ -25,6 +25,7 @@ from collections.abc import Callable
from pathlib import Path
from typing import Any, Self
from ahriman.core.configuration.shell_interpolator import ShellInterpolator
from ahriman.core.exceptions import InitializeError
from ahriman.models.repository_paths import RepositoryPaths
@ -48,7 +49,7 @@ class Configuration(configparser.RawConfigParser):
>>> from pathlib import Path
>>>
>>> configuration = Configuration.from_path(Path("/etc/ahriman.ini"), "x86_64", quiet=False)
>>> configuration = Configuration.from_path(Path("/etc/ahriman.ini"), "x86_64")
>>> repository_name = configuration.get("repository", "name")
>>> makepkg_flags = configuration.getlist("build", "makepkg_flags")
@ -73,10 +74,16 @@ class Configuration(configparser.RawConfigParser):
allow_no_value(bool, optional): copies ``configparser.RawConfigParser`` behaviour. In case if it is set
to ``True``, the keys without values will be allowed (Default value = False)
"""
configparser.RawConfigParser.__init__(self, allow_no_value=allow_no_value, converters={
"list": shlex.split,
"path": self._convert_path,
})
configparser.RawConfigParser.__init__(
self,
allow_no_value=allow_no_value,
interpolation=ShellInterpolator(),
converters={
"list": shlex.split,
"path": self._convert_path,
}
)
self.architecture: str | None = None
self.path: Path | None = None
self.includes: list[Path] = []

View File

@ -93,9 +93,12 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"type": "string",
"oneof": [
{"allowed": ["disabled"]},
{"allowed": ["configuration", "mapping"], "dependencies": ["salt"]},
{"allowed": ["configuration", "mapping"]},
{"allowed": ["oauth"], "dependencies": [
"client_id", "client_secret", "oauth_provider", "oauth_scopes", "salt"
"client_id",
"client_secret",
"oauth_provider",
"oauth_scopes",
]},
],
},
@ -225,6 +228,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"coerce": "list",
"schema": {"type": "string"},
},
"enable_archive_upload": {
"type": "boolean",
"coerce": "boolean",
},
"host": {
"type": "string",
"is_ip_address": ["localhost"],
@ -233,6 +240,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"type": "string",
"is_url": ["http", "https"],
},
"max_body_size": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"password": {
"type": "string",
},
@ -254,6 +266,11 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"required": True,
"path_exists": True,
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"unix_socket": {
"type": "path",
"coerce": "absolute_path",
@ -265,6 +282,10 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"username": {
"type": "string",
},
"wait_timeout": {
"type": "integer",
"coerce": "integer",
}
},
},
}

View File

@ -0,0 +1,51 @@
#
# Copyright (c) 2021-2023 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 configparser
import os
from collections.abc import Mapping, MutableMapping
from string import Template
class ShellInterpolator(configparser.Interpolation):
"""
custom string interpolator, because we cannot use defaults argument due to config validation
"""
def before_get(self, parser: MutableMapping[str, Mapping[str, str]], section: str, option: str, value: str,
defaults: Mapping[str, str]) -> str:
"""
interpolate option value
Args:
parser(MutableMapping[str, Mapping[str, str]]): option parser
section(str): section name
option(str): option name
value(str): source (not-converted) value
defaults(Mapping[str, str]): default values
Returns:
str: substituted value
"""
# At the moment it seems that it is the most legit way to handle environment variables
# Template behaviour is literally the same as shell
# In addition, we are using shell-like variables in some cases (see ``alpm.mirror`` option), thus we would like
# to keep them alive
return Template(value).safe_substitute(os.environ)

View File

@ -136,31 +136,13 @@ class Validator(RootValidator):
The rule's arguments are validated against this schema:
{"type": "list", "schema": {"type": "string"}}
"""
url = urlparse(value) # it probably will never rise exceptions on parse
if not url.scheme:
self._error(field, f"Url scheme is not set for {value}")
if not url.netloc and url.scheme not in ("file",):
self._error(field, f"Location must be set for url {value} of scheme {url.scheme}")
if constraint and url.scheme not in constraint:
self._error(field, f"Url {value} scheme must be one of {constraint}")
def _validate_path_is_absolute(self, constraint: bool, field: str, value: Path) -> None:
"""
check if path is absolute or not
Args:
constraint(bool): True in case if path must be absolute and False if it must be relative
field(str): field name to be checked
value(Path): value to be checked
Examples:
The rule's arguments are validated against this schema:
{"type": "boolean"}
"""
if constraint and not value.is_absolute():
self._error(field, f"Path {value} must be absolute")
if not constraint and value.is_absolute():
self._error(field, f"Path {value} must be relative")
match urlparse(value): # it probably will never rise exceptions on parse
case url if not url.scheme:
self._error(field, f"Url scheme is not set for {value}")
case url if not url.netloc and url.scheme not in ("file",):
self._error(field, f"Location must be set for url {value} of scheme {url.scheme}")
case url if constraint and url.scheme not in constraint:
self._error(field, f"Url {value} scheme must be one of {constraint}")
def _validate_path_exists(self, constraint: bool, field: str, value: Path) -> None:
"""
@ -175,7 +157,8 @@ class Validator(RootValidator):
The rule's arguments are validated against this schema:
{"type": "boolean"}
"""
if constraint and not value.exists():
self._error(field, f"Path {value} must exist")
if not constraint and value.exists():
self._error(field, f"Path {value} must not exist")
match value.exists():
case True if not constraint:
self._error(field, f"Path {value} must not exist")
case False if constraint:
self._error(field, f"Path {value} must exist")

View File

@ -70,6 +70,7 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N
connection(Connection): database connection
paths(RepositoryPaths): repository paths instance
"""
from ahriman.core.alpm.remote import AUR
from ahriman.core.database.operations import PackageOperations
def insert_remote(base: str, remote: RemoteSource) -> None:
@ -92,7 +93,11 @@ def migrate_package_remotes(connection: Connection, paths: RepositoryPaths) -> N
local_cache = paths.cache_for(package_base)
if local_cache.exists() and not package.is_vcs:
continue # skip packages which are not VCS and with local cache
remote_source = RemoteSource.from_source(PackageSource.AUR, package_base, "aur")
if remote_source is None:
continue # should never happen
remote_source = RemoteSource(
source=PackageSource.AUR,
git_url=AUR.remote_git_url(package_base, "aur"),
web_url=AUR.remote_web_url(package_base),
path=".",
branch="master",
)
insert_remote(package_base, remote_source)

View File

@ -66,7 +66,7 @@ def migrate_package_depends(connection: Connection, configuration: Configuration
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman, remote=None)
base = Package.from_archive(full_path, pacman)
for package, description in base.packages.items():
package_list.append({
"make_depends": description.make_depends,

View File

@ -63,7 +63,7 @@ def migrate_package_check_depends(connection: Connection, configuration: Configu
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
base = Package.from_archive(full_path, pacman, remote=None)
base = Package.from_archive(full_path, pacman)
for package, description in base.packages.items():
package_list.append({
"check_depends": description.check_depends,

View File

@ -69,7 +69,7 @@ def migrate_package_base_packager(connection: Connection, configuration: Configu
package_list = []
for full_path in filter(package_like, configuration.repository_paths.repository.iterdir()):
package = Package.from_archive(full_path, pacman, remote=None)
package = Package.from_archive(full_path, pacman)
package_list.append({
"package_base": package.base,
"packager": package.packager,

View File

@ -0,0 +1,27 @@
#
# Copyright (c) 2021-2023 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 = [
"""
update package_bases set source = 'local' where source is null
""",
]

View File

@ -0,0 +1,36 @@
#
# Copyright (c) 2021-2023 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 = [
"""
drop index logs_package_base_process_id
""",
"""
alter table logs drop column process_id
""",
"""
alter table logs add column version text not null default ''
""",
"""
create index logs_package_base_version on logs (package_base, version)
""",
]

View File

@ -66,13 +66,13 @@ class LogsOperations(Operations):
connection.execute(
"""
insert into logs
(package_base, process_id, created, record)
(package_base, version, created, record)
values
(:package_base, :process_id, :created, :record)
(:package_base, :version, :created, :record)
""",
{
"package_base": log_record_id.package_base,
"process_id": log_record_id.process_id,
"version": log_record_id.version,
"created": created,
"record": record,
}
@ -80,22 +80,22 @@ class LogsOperations(Operations):
return self.with_connection(run, commit=True)
def logs_remove(self, package_base: str, current_process_id: int | None) -> None:
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove log records for the specified package
Args:
package_base(str): package base to remove logs
current_process_id(int | None): current process id. If set it will remove only logs belonging to another
process
version(str): package version. If set it will remove only logs belonging to another
version
"""
def run(connection: Connection) -> None:
connection.execute(
"""
delete from logs
where package_base = :package_base and (:process_id is null or process_id <> :process_id)
where package_base = :package_base and (:version is null or version <> :version)
""",
{"package_base": package_base, "process_id": current_process_id}
{"package_base": package_base, "version": version}
)
return self.with_connection(run, commit=True)

View File

@ -86,11 +86,11 @@ class PackageOperations(Operations):
{
"package_base": package.base,
"version": package.version,
"branch": package.remote.branch if package.remote is not None else None,
"git_url": package.remote.git_url if package.remote is not None else None,
"path": package.remote.path if package.remote is not None else None,
"web_url": package.remote.web_url if package.remote is not None else None,
"source": package.remote.source.value if package.remote is not None else None,
"branch": package.remote.branch,
"git_url": package.remote.git_url,
"path": package.remote.path,
"web_url": package.remote.web_url,
"source": package.remote.source.value,
"packager": package.packager,
}
)
@ -270,5 +270,4 @@ class PackageOperations(Operations):
return {
package_base: package.remote
for package_base, package in packages.items()
if package.remote is not None
}

View File

@ -17,8 +17,11 @@
# 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 subprocess
from collections.abc import Callable
from pathlib import Path
from typing import Any
from typing import Any, Self
class BuildError(RuntimeError):
@ -26,14 +29,61 @@ class BuildError(RuntimeError):
base exception for failed builds
"""
def __init__(self, package_base: str) -> None:
def __init__(self, package_base: str, stderr: str | None = None) -> None:
"""
default constructor
Args:
package_base(str): package base raised exception
stderr(str | None, optional): stderr of the process if available (Default value = None)
"""
RuntimeError.__init__(self, f"Package {package_base} build failed, check logs for details")
message = f"Package {package_base} build failed,\n"
if stderr is not None:
message += f"process stderr:\n{stderr}\n"
message += "check logs for details"
RuntimeError.__init__(self, message)
@classmethod
def from_process(cls, package_base: str) -> Callable[[int, list[str], str, str], Self]:
"""
generate exception callable from process error
Args:
package_base(str): package base raised exception
Returns:
Callable[[int, list[str], str, str], Self]: exception generator to be passed to subprocess utils
"""
return lambda code, process, stdout, stderr: cls(package_base, stderr)
class CalledProcessError(subprocess.CalledProcessError):
"""
like ``subprocess.CalledProcessError``, but better
"""
def __init__(self, status_code: int, process: list[str], stderr: str) -> None:
"""
default constructor
Args:
status_code(int): process return code
process(list[str]): process argument list
stderr(str): stderr of the process
"""
subprocess.CalledProcessError.__init__(self, status_code, process, stderr=stderr)
def __str__(self) -> str:
"""
string representation of the exception
Returns:
str: string view of the exception
"""
return f"""{subprocess.CalledProcessError.__str__(self)}
Process stderr:
{self.stderr}"""
class DuplicateRunError(RuntimeError):
@ -194,6 +244,21 @@ class PasswordError(ValueError):
ValueError.__init__(self, f"Password error: {details}")
class PartitionError(RuntimeError):
"""
exception raised during packages partition actions
"""
def __init__(self, count: int) -> None:
"""
default constructor
Args:
count(int): count of partitions
"""
RuntimeError.__init__(self, f"Could not divide packages into {count} partitions")
class PkgbuildGeneratorError(RuntimeError):
"""
exception class for support type triggers

View File

@ -38,7 +38,7 @@ class TreePrinter(StringPrinter):
level(int): dependencies tree level
packages(list[Package]): packages which belong to this level
"""
StringPrinter.__init__(self, f"level {level}")
StringPrinter.__init__(self, f"level #{level}")
self.packages = packages
def properties(self) -> list[Property]:

View File

@ -104,9 +104,12 @@ class RemotePull(LazyLogging):
def run(self) -> None:
"""
run git pull action
Raises:
GitRemoteError: pull processing error
"""
try:
self.repo_clone()
except Exception:
self.logger.exception("git pull failed")
raise GitRemoteError()
raise GitRemoteError

View File

@ -39,7 +39,7 @@ class RemotePush(LazyLogging):
sync PKGBUILDs to remote repository after actions
Attributes:
commit_author(str | None): optional commit author in form of git config (i.e. ``user <user@host>``)
commit_author(tuple[str, str] | None): optional commit author in form of git config
database(SQLite): database instance
remote_source(RemoteSource): repository remote source (remote pull url and branch)
"""
@ -54,7 +54,11 @@ class RemotePush(LazyLogging):
section(str): settings section name
"""
self.database = database
self.commit_author = configuration.get(section, "commit_author", fallback=None)
commit_email = configuration.get(section, "commit_email", fallback="ahriman@localhost")
commit_user = configuration.get(section, "commit_user", fallback="ahriman")
self.commit_author = (commit_user, commit_email)
self.remote_source = RemoteSource(
git_url=configuration.get(section, "push_url"),
web_url="",
@ -114,6 +118,9 @@ class RemotePush(LazyLogging):
Args:
result(Result): build result
Raises:
GitRemoteError: push processing error
"""
try:
with TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
@ -123,4 +130,4 @@ class RemotePush(LazyLogging):
commit_author=self.commit_author)
except Exception:
self.logger.exception("git push failed")
raise GitRemoteError()
raise GitRemoteError

View File

@ -49,7 +49,10 @@ class RemotePushTrigger(Trigger):
"gitremote": {
"type": "dict",
"schema": {
"commit_author": {
"commit_email": {
"type": "string",
},
"commit_user": {
"type": "string",
},
"push_url": {

View File

@ -17,4 +17,4 @@
# 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.10.2"
from ahriman.core.http.sync_http_client import MultipartType, SyncHttpClient

View File

@ -0,0 +1,137 @@
#
# Copyright (c) 2021-2023 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 requests
from functools import cached_property
from typing import Any, IO, Literal
from ahriman import __version__
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
# filename, file, content-type, headers
MultipartType = tuple[str, IO[bytes], str, dict[str, str]]
class SyncHttpClient(LazyLogging):
"""
wrapper around requests library to reduce boilerplate
Attributes:
auth(tuple[str, str] | None): HTTP basic auth object if set
suppress_errors(bool): suppress logging of request errors
timeout(int): HTTP request timeout in seconds
"""
def __init__(self, section: str | None = None, configuration: Configuration | None = None, *,
suppress_errors: bool = False) -> None:
"""
default constructor
Args:
section(str, optional): settings section name (Default value = None)
configuration(Configuration | None): configuration instance (Default value = None)
suppress_errors(bool, optional): suppress logging of request errors (Default value = False)
"""
if configuration is None:
configuration = Configuration() # dummy configuration
if section is None:
section = configuration.default_section
username = configuration.get(section, "username", fallback=None)
password = configuration.get(section, "password", fallback=None)
self.auth = (username, password) if username and password else None
self.timeout = configuration.getint(section, "timeout", fallback=30)
self.suppress_errors = suppress_errors
@cached_property
def session(self) -> requests.Session:
"""
get or create session
Returns:
request.Session: created session object
"""
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session
@staticmethod
def exception_response_text(exception: requests.exceptions.RequestException) -> str:
"""
safe response exception text generation
Args:
exception(requests.exceptions.RequestException): exception raised
Returns:
str: text of the response if it is not None and empty string otherwise
"""
result: str = exception.response.text if exception.response is not None else ""
return result
def make_request(self, method: Literal["DELETE", "GET", "POST", "PUT"], url: str, *,
headers: dict[str, str] | None = None,
params: list[tuple[str, str]] | None = None,
data: Any | None = None,
json: dict[str, Any] | None = None,
files: dict[str, MultipartType] | None = None,
session: requests.Session | None = None,
suppress_errors: bool | None = None) -> requests.Response:
"""
perform request with specified parameters
Args:
method(Literal["DELETE", "GET", "POST", "PUT"]): HTTP method to call
url(str): remote url to call
headers(dict[str, str] | None, optional): request headers (Default value = None)
params(list[tuple[str, str]] | None, optional): request query parameters (Default value = None)
data(Any | None, optional): request raw data parameters (Default value = None)
json(dict[str, Any] | None, optional): request json parameters (Default value = None)
files(dict[str, MultipartType] | None, optional): multipart upload (Default value = None)
session(requests.Session | None, optional): session object if any (Default value = None)
suppress_errors(bool | None, optional): suppress logging errors (e.g. if no web server available). If none
set, the instance-wide value will be used (Default value = None)
Returns:
requests.Response: response object
"""
# defaults
if suppress_errors is None:
suppress_errors = self.suppress_errors
if session is None:
session = self.session
try:
response = session.request(method, url, params=params, data=data, headers=headers, files=files, json=json,
auth=self.auth, timeout=self.timeout)
response.raise_for_status()
return response
except requests.HTTPError as ex:
if not suppress_errors:
self.logger.exception("could not perform http request: %s", self.exception_response_text(ex))
raise
except Exception:
if not suppress_errors:
self.logger.exception("could not perform http request")
raise

View File

@ -30,13 +30,14 @@ class FilteredAccessLogger(AccessLogger):
LOG_PATH_REGEX(re.Pattern): (class attribute) regex for logs uri
"""
# official packages have only ``[A-Za-z0-9_.+-]`` regex
LOG_PATH_REGEX = re.compile(r"^/api/v1/packages/[A-Za-z0-9_.+%-]+/logs$")
LOG_PATH_REGEX = re.compile(r"^/api/v1/packages/[^/]+/logs$")
# technically process id is uuid, but we might change it later
PROCESS_PATH_REGEX = re.compile(r"^/api/v1/service/process/[^/]+$")
@staticmethod
def is_logs_post(request: BaseRequest) -> bool:
"""
check if request looks lie logs posting
check if request looks like logs posting
Args:
request(BaseRequest): http reqeust descriptor
@ -46,6 +47,19 @@ class FilteredAccessLogger(AccessLogger):
"""
return request.method == "POST" and FilteredAccessLogger.LOG_PATH_REGEX.match(request.path) is not None
@staticmethod
def is_process_get(request: BaseRequest) -> bool:
"""
check if request looks like process status request
Args:
request(BaseRequest): http reqeust descriptor
Returns:
bool: True in case if request looks like process status request and False otherwise
"""
return request.method == "GET" and FilteredAccessLogger.PROCESS_PATH_REGEX.match(request.path) is not None
def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None:
"""
access log with enabled filter by request path
@ -55,6 +69,7 @@ class FilteredAccessLogger(AccessLogger):
response(StreamResponse): streaming response object
time(float):
"""
if self.is_logs_post(request):
if self.is_logs_post(request) \
or self.is_process_get(request):
return
AccessLogger.log(self, request, response, time)

View File

@ -81,12 +81,12 @@ class HttpLogHandler(logging.Handler):
Args:
record(logging.LogRecord): log record to log
"""
package_base = getattr(record, "package_base", None)
if package_base is None:
log_record_id = getattr(record, "package_id", None)
if log_record_id is None:
return # in case if no package base supplied we need just skip log message
try:
self.reporter.logs(package_base, record)
self.reporter.package_logs(log_record_id, record)
except Exception:
if self.suppress_errors:
return

View File

@ -24,6 +24,8 @@ from collections.abc import Generator
from functools import cached_property
from typing import Any
from ahriman.models.log_record_id import LogRecordId
class LazyLogging:
"""
@ -60,38 +62,40 @@ class LazyLogging:
logging.setLogRecordFactory(logging.LogRecord)
@staticmethod
def _package_logger_set(package_base: str) -> None:
def _package_logger_set(package_base: str, version: str | None) -> None:
"""
set package base as extra info to the logger
Args:
package_base(str): package base
version(str | None): package version if available
"""
current_factory = logging.getLogRecordFactory()
def package_record_factory(*args: Any, **kwargs: Any) -> logging.LogRecord:
record = current_factory(*args, **kwargs)
record.package_base = package_base
record.package_id = LogRecordId(package_base, version or "")
return record
logging.setLogRecordFactory(package_record_factory)
@contextlib.contextmanager
def in_package_context(self, package_base: str) -> Generator[None, None, None]:
def in_package_context(self, package_base: str, version: str | None) -> Generator[None, None, None]:
"""
execute function while setting package context
Args:
package_base(str): package base to set context in
version(str | None): package version if available
Examples:
This function is designed to be called as context manager with ``package_base`` argument, e.g.:
>>> with self.in_package_context(package.base):
>>> with self.in_package_context(package.base, package.version):
>>> build_package(package)
"""
try:
self._package_logger_set(package_base)
self._package_logger_set(package_base, version)
yield
finally:
self._package_logger_reset()

View File

@ -0,0 +1,120 @@
#
# Copyright (c) 2021-2023 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 requests
from ahriman.core.configuration import Configuration
from ahriman.core.report.report import Report
from ahriman.core.status.web_client import WebClient
from ahriman.models.package import Package
from ahriman.models.result import Result
from ahriman.models.waiter import Waiter
class RemoteCall(Report):
"""
trigger implementation which call remote service with update
Attributes:
client(WebClient): web client instance
update_aur(bool): check for AUR updates
update_local(bool): check for local packages update
update_manual(bool): check for manually built packages
wait_timeout(int): timeout to wait external process
"""
def __init__(self, architecture: str, configuration: Configuration, section: str) -> None:
"""
default constructor
Args:
architecture(str): repository architecture
configuration(Configuration): configuration instance
section(str): settings section name
"""
Report.__init__(self, architecture, configuration)
self.client = WebClient(configuration)
self.update_aur = configuration.getboolean(section, "aur", fallback=False)
self.update_local = configuration.getboolean(section, "local", fallback=False)
self.update_manual = configuration.getboolean(section, "manual", fallback=False)
self.wait_timeout = configuration.getint(section, "wait_timeout", fallback=-1)
def generate(self, packages: list[Package], result: Result) -> None:
"""
generate report for the specified packages
Args:
packages(list[Package]): list of packages to generate report
result(Result): build result
"""
process_id = self.remote_update()
self.remote_wait(process_id)
def is_process_alive(self, process_id: str) -> bool:
"""
check if process is alive
Args:
process_id(str): remote process id
Returns:
bool: True in case if remote process is alive and False otherwise
"""
try:
response = self.client.make_request("GET", f"/api/v1/service/process/{process_id}")
except requests.HTTPError as ex:
status_code = ex.response.status_code if ex.response is not None else None
if status_code == 404:
return False
raise
response_json = response.json()
is_alive: bool = response_json["is_alive"]
return is_alive
def remote_update(self) -> str:
"""
call remote server for update
Returns:
str: remote process id
"""
response = self.client.make_request("POST", "/api/v1/service/update", json={
"aur": self.update_aur,
"local": self.update_local,
"manual": self.update_manual,
})
response_json = response.json()
process_id: str = response_json["process_id"]
return process_id
def remote_wait(self, process_id: str) -> None:
"""
wait for remote process termination
Args:
process_id(str): remote process id
"""
waiter = Waiter(self.wait_timeout)
waiter.wait(self.is_process_alive, process_id)

View File

@ -80,20 +80,24 @@ class Report(LazyLogging):
Report: client according to current settings
"""
section, provider_name = configuration.gettype(target, architecture)
provider = ReportSettings.from_option(provider_name)
if provider == ReportSettings.HTML:
from ahriman.core.report.html import HTML
return HTML(architecture, configuration, section)
if provider == ReportSettings.Email:
from ahriman.core.report.email import Email
return Email(architecture, configuration, section)
if provider == ReportSettings.Console:
from ahriman.core.report.console import Console
return Console(architecture, configuration, section)
if provider == ReportSettings.Telegram:
from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section)
return Report(architecture, configuration) # should never happen
match ReportSettings.from_option(provider_name):
case ReportSettings.HTML:
from ahriman.core.report.html import HTML
return HTML(architecture, configuration, section)
case ReportSettings.Email:
from ahriman.core.report.email import Email
return Email(architecture, configuration, section)
case ReportSettings.Console:
from ahriman.core.report.console import Console
return Console(architecture, configuration, section)
case ReportSettings.Telegram:
from ahriman.core.report.telegram import Telegram
return Telegram(architecture, configuration, section)
case ReportSettings.RemoteCall:
from ahriman.core.report.remote_call import RemoteCall
return RemoteCall(architecture, configuration, section)
case _:
return Report(architecture, configuration) # should never happen
def generate(self, packages: list[Package], result: Result) -> None:
"""
@ -113,10 +117,10 @@ class Report(LazyLogging):
packages(list[Package]): list of packages to generate report
Raises:
ReportFailed: in case of any report unmatched exception
ReportError: in case of any report unmatched exception
"""
try:
self.generate(packages, result)
except Exception:
self.logger.exception("report generation failed")
raise ReportError()
raise ReportError

View File

@ -191,6 +191,31 @@ class ReportTrigger(Trigger):
},
},
},
"remote-call": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["ahriman", "remote-call"],
},
"aur": {
"type": "boolean",
"coerce": "boolean",
},
"local": {
"type": "boolean",
"coerce": "boolean",
},
"manual": {
"type": "boolean",
"coerce": "boolean",
},
"wait_timeout": {
"type": "integer",
"coerce": "integer",
},
},
}
}
def __init__(self, architecture: str, configuration: Configuration) -> None:

View File

@ -17,17 +17,15 @@
# 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 requests # technically we could use python-telegram-bot, but it is just a single request, c'mon
from ahriman.core.configuration import Configuration
from ahriman.core.http import SyncHttpClient
from ahriman.core.report.jinja_template import JinjaTemplate
from ahriman.core.report.report import Report
from ahriman.core.util import exception_response_text
from ahriman.models.package import Package
from ahriman.models.result import Result
class Telegram(Report, JinjaTemplate):
class Telegram(Report, JinjaTemplate, SyncHttpClient):
"""
telegram report generator
@ -38,7 +36,6 @@ class Telegram(Report, JinjaTemplate):
chat_id(str): chat id to post message, either string with @ or integer
template_path(Path): path to template for built packages
template_type(str): template message type to be used in parse mode, one of MarkdownV2, HTML, Markdown
timeout(int): HTTP request timeout in seconds
"""
TELEGRAM_API_URL = "https://api.telegram.org"
@ -55,12 +52,12 @@ class Telegram(Report, JinjaTemplate):
"""
Report.__init__(self, architecture, configuration)
JinjaTemplate.__init__(self, section, configuration)
SyncHttpClient.__init__(self, section, configuration)
self.api_key = configuration.get(section, "api_key")
self.chat_id = configuration.get(section, "chat_id")
self.template_path = configuration.getpath(section, "template_path")
self.template_type = configuration.get(section, "template_type", fallback="HTML")
self.timeout = configuration.getint(section, "timeout", fallback=30)
def _send(self, text: str) -> None:
"""
@ -69,18 +66,8 @@ class Telegram(Report, JinjaTemplate):
Args:
text(str): message body text
"""
try:
response = requests.post(
f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type},
timeout=self.timeout)
response.raise_for_status()
except requests.HTTPError as e:
self.logger.exception("could not perform request: %s", exception_response_text(e))
raise
except Exception:
self.logger.exception("could not perform request")
raise
self.make_request("POST", f"{self.TELEGRAM_API_URL}/bot{self.api_key}/sendMessage",
data={"chat_id": self.chat_id, "text": text, "parse_mode": self.template_type})
def generate(self, packages: list[Package], result: Result) -> None:
"""
@ -89,6 +76,9 @@ class Telegram(Report, JinjaTemplate):
Args:
packages(list[Package]): list of packages to generate report
result(Result): build result
Raises:
ValueError: impossible to split message by chunks
"""
if not result.success:
return

View File

@ -64,7 +64,8 @@ class Executor(Cleaner):
"""
raise NotImplementedError
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None) -> Result:
def process_build(self, updates: Iterable[Package], packagers: Packagers | None = None, *,
bump_pkgrel: bool = False) -> Result:
"""
build packages
@ -72,24 +73,28 @@ class Executor(Cleaner):
updates(Iterable[Package]): list of packages properties to build
packagers(Packagers | None, optional): optional override of username for build process
(Default value = None)
bump_pkgrel(bool, optional): bump pkgrel in case of local version conflict (Default value = False)
Returns:
Result: build result
"""
def build_single(package: Package, local_path: Path, packager_id: str | None) -> None:
self.reporter.set_building(package.base)
task = Task(package, self.configuration, self.paths)
task.init(local_path, self.database)
task = Task(package, self.configuration, self.architecture, self.paths)
local_version = local_versions.get(package.base) if bump_pkgrel else None
task.init(local_path, self.database, local_version)
built = task.build(local_path, packager_id)
for src in built:
dst = self.paths.packages / src.name
shutil.move(src, dst)
packagers = packagers or Packagers()
local_versions = {package.base: package.version for package in self.packages()}
result = Result()
for single in updates:
with self.in_package_context(single.base), TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
with self.in_package_context(single.base, local_versions.get(single.base)), \
TemporaryDirectory(ignore_cleanup_errors=True) as dir_name:
try:
packager = self.packager(packagers, single.base)
build_single(single, Path(dir_name), packager.packager_id)
@ -117,7 +122,7 @@ class Executor(Cleaner):
self.database.build_queue_clear(package_base)
self.database.patches_remove(package_base, [])
self.database.logs_remove(package_base, None)
self.reporter.remove(package_base) # we only update status page in case of base removal
self.reporter.package_remove(package_base) # we only update status page in case of base removal
except Exception:
self.logger.exception("could not remove base %s", package_base)
@ -197,14 +202,16 @@ class Executor(Cleaner):
package_path = self.paths.repository / safe_filename(name)
self.repo.add(package_path)
current_packages = self.packages()
current_packages = {package.base: package for package in self.packages()}
local_versions = {package_base: package.version for package_base, package in current_packages.items()}
removed_packages: list[str] = [] # list of packages which have been removed from the base
updates = self.load_archives(packages)
packagers = packagers or Packagers()
result = Result()
for local in updates:
with self.in_package_context(local.base):
with self.in_package_context(local.base, local_versions.get(local.base)):
try:
packager = self.packager(packagers, local.base)
@ -214,12 +221,9 @@ class Executor(Cleaner):
self.reporter.set_success(local)
result.add_success(local)
current_package_archives = {
package
for current in current_packages
if current.base == local.base
for package in current.packages
}
current_package_archives: set[str] = set()
if local.base in current_packages:
current_package_archives = set(current_packages[local.base].packages.keys())
removed_packages.extend(current_package_archives.difference(local.packages))
except Exception:
self.reporter.set_failed(local.base)

View File

@ -40,7 +40,7 @@ class Repository(Executor, UpdateHandler):
Examples:
This class along with traits provides access to local repository actions, e.g. remove packages, update packages,
sync local repository to remote, generate report, etc::
sync local repository to remote, generate report, etc.::
>>> from ahriman.core.configuration import Configuration
>>> from ahriman.core.database import SQLite
@ -117,8 +117,9 @@ class Repository(Executor, UpdateHandler):
# we are iterating over bases, not single packages
for full_path in packages:
try:
local = Package.from_archive(full_path, self.pacman, None)
local.remote = sources.get(local.base)
local = Package.from_archive(full_path, self.pacman)
if (source := sources.get(local.base)) is not None:
local.remote = source
current = result.setdefault(local.base, local)
if current.version != local.version:

View File

@ -24,6 +24,7 @@ from ahriman.core.exceptions import UnknownPackageError
from ahriman.core.repository.cleaner import Cleaner
from ahriman.models.package import Package
from ahriman.models.package_source import PackageSource
from ahriman.models.remote_source import RemoteSource
class UpdateHandler(Cleaner):
@ -55,22 +56,23 @@ class UpdateHandler(Cleaner):
list[Package]: list of packages which are out-of-dated
"""
def load_remote(package: Package) -> Package:
source = package.remote.source if package.remote is not None else None
# try to load package from base and if none found try to load by separated packages
for probe in [package.base] + sorted(package.packages.keys()):
try:
if source == PackageSource.Repository:
if package.remote.source == PackageSource.Repository:
return Package.from_official(probe, self.pacman, None)
return Package.from_aur(probe, self.pacman, None)
except UnknownPackageError:
continue
raise UnknownPackageError(package.base)
result: list[Package] = []
local_versions = {package.base: package.version for package in self.packages()}
result: list[Package] = []
for local in self.packages():
with self.in_package_context(local.base):
with self.in_package_context(local.base, local_versions.get(local.base)):
if not local.remote.is_remote:
continue # avoid checking local packages
if local.base in self.ignore_list:
continue
if filter_packages and local.base not in filter_packages:
@ -101,13 +103,22 @@ class UpdateHandler(Cleaner):
Returns:
list[Package]: list of local packages which are out-of-dated
"""
result: list[Package] = []
packages = {local.base: local for local in self.packages()}
local_versions = {package_base: package.version for package_base, package in packages.items()}
result: list[Package] = []
for cache_dir in self.paths.cache.iterdir():
with self.in_package_context(cache_dir.name):
with self.in_package_context(cache_dir.name, local_versions.get(cache_dir.name)):
try:
Sources.fetch(cache_dir, remote=None)
source = RemoteSource(
source=PackageSource.Local,
git_url=cache_dir.absolute().as_uri(),
web_url="",
path=".",
branch="master",
)
Sources.fetch(cache_dir, source)
remote = Package.from_build(cache_dir, self.architecture, None)
local = packages.get(remote.base)

View File

@ -17,30 +17,26 @@
# 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 requests
from pathlib import Path
from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import BuildError
from ahriman.core.log import LazyLogging
from ahriman.core.util import check_output, exception_response_text
from ahriman.core.http import SyncHttpClient
from ahriman.core.util import check_output
from ahriman.models.sign_settings import SignSettings
class GPG(LazyLogging):
class GPG(SyncHttpClient):
"""
gnupg wrapper
Attributes:
DEFAULT_TIMEOUT(int): (class attribute) HTTP request timeout in seconds
configuration(Configuration): configuration instance
default_key(str | None): default PGP key ID to use
targets(set[SignSettings]): list of targets to sign (repository, package etc)
targets(set[SignSettings]): list of targets to sign (repository, package etc.)
"""
_check_output = check_output
DEFAULT_TIMEOUT = 30
def __init__(self, configuration: Configuration) -> None:
"""
@ -49,6 +45,7 @@ class GPG(LazyLogging):
Args:
configuration(Configuration): configuration instance
"""
SyncHttpClient.__init__(self)
self.configuration = configuration
self.targets, self.default_key = self.sign_options(configuration)
@ -101,28 +98,36 @@ class GPG(LazyLogging):
default_key = configuration.get("sign", "key") if targets else None
return targets, default_key
@staticmethod
def signature(filepath: Path) -> Path:
"""
generate signature name for the file
Args:
filepath(Path): path to the file which will be signed
Returns:
str: path to signature file
"""
return filepath.parent / f"{filepath.name}.sig"
def key_download(self, server: str, key: str) -> str:
"""
download key from public PGP server
Args:
server(str): public PGP server which will be used to download the key
server(str): public PGP server which will be used to download data
key(str): key ID to download
Returns:
str: key as plain text
"""
key = key if key.startswith("0x") else f"0x{key}"
try:
response = requests.get(f"https://{server}/pks/lookup", params={
"op": "get",
"options": "mr",
"search": key
}, timeout=self.DEFAULT_TIMEOUT)
response.raise_for_status()
except requests.exceptions.HTTPError as e:
self.logger.exception("could not download key %s from %s: %s", key, server, exception_response_text(e))
raise
response = self.make_request("GET", f"https://{server}/pks/lookup", params=[
("op", "get"),
("options", "mr"),
("search", key),
])
return response.text
def key_export(self, key: str) -> str:
@ -158,7 +163,7 @@ class GPG(LazyLogging):
import key to current user and sign it locally
Args:
server(str): public PGP server which will be used to download the key
server(str): public PGP server which will be used to download data
key(str): key ID to import
"""
key_body = self.key_download(server, key)
@ -177,13 +182,13 @@ class GPG(LazyLogging):
"""
GPG._check_output(
*GPG.sign_command(path, key),
exception=BuildError(path.name),
exception=BuildError.from_process(path.name),
logger=self.logger)
return [path, path.parent / f"{path.name}.sig"]
return [path, self.signature(path)]
def process_sign_package(self, path: Path, packager_key: str | None) -> list[Path]:
"""
sign package if required by configuration
sign package if required by configuration and signature doesn't exist
Args:
path(Path): path to file to sign
@ -192,6 +197,10 @@ class GPG(LazyLogging):
Returns:
list[Path]: list of generated files including original file
"""
if (signature := self.signature(path)).is_file():
# the file was already signed before, just use its signature
return [path, signature]
if SignSettings.Packages not in self.targets:
return [path]

View File

@ -20,6 +20,7 @@
from __future__ import annotations
import argparse
import time
import uuid
from collections.abc import Callable, Iterable
@ -27,7 +28,6 @@ from multiprocessing import Process, Queue
from threading import Lock, Thread
from ahriman.core.log import LazyLogging
from ahriman.models.package_source import PackageSource
class Spawn(Thread, LazyLogging):
@ -39,7 +39,7 @@ class Spawn(Thread, LazyLogging):
active(dict[str, Process]): map of active child processes required to avoid zombies
architecture(str): repository architecture
command_arguments(list[str]): base command line arguments
queue(Queue[tuple[str, bool]]): multiprocessing queue to read updates from processes
queue(Queue[tuple[str, bool, int]]): multiprocessing queue to read updates from processes
"""
def __init__(self, args_parser: argparse.ArgumentParser, architecture: str, command_arguments: list[str]) -> None:
@ -60,11 +60,25 @@ class Spawn(Thread, LazyLogging):
self.lock = Lock()
self.active: dict[str, Process] = {}
# stupid pylint does not know that it is possible
self.queue: Queue[tuple[str, bool] | None] = Queue() # pylint: disable=unsubscriptable-object
self.queue: Queue[tuple[str, bool, int] | None] = Queue() # pylint: disable=unsubscriptable-object
@staticmethod
def boolean_action_argument(name: str, value: bool) -> str:
"""
convert option of given name with value to boolean action argument
Args:
name(str): command line argument name
value(bool): command line argument value
Returns:
str: if ``value`` is True, then returns positive flag and negative otherwise
"""
return name if value else f"no-{name}"
@staticmethod
def process(callback: Callable[[argparse.Namespace, str], bool], args: argparse.Namespace, architecture: str,
process_id: str, queue: Queue[tuple[str, bool]]) -> None: # pylint: disable=unsubscriptable-object
process_id: str, queue: Queue[tuple[str, bool, int]]) -> None: # pylint: disable=unsubscriptable-object
"""
helper to run external process
@ -73,12 +87,17 @@ class Spawn(Thread, LazyLogging):
args(argparse.Namespace): command line arguments
architecture(str): repository architecture
process_id(str): process unique identifier
queue(Queue[tuple[str, bool]]): output queue
queue(Queue[tuple[str, bool, int]]): output queue
"""
start_time = time.monotonic()
result = callback(args, architecture)
queue.put((process_id, result))
stop_time = time.monotonic()
def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> None:
consumed_time = int(1000 * (stop_time - start_time))
queue.put((process_id, result, consumed_time))
def _spawn_process(self, command: str, *args: str, **kwargs: str | None) -> str:
"""
spawn external ahriman process with supplied arguments
@ -86,6 +105,9 @@ class Spawn(Thread, LazyLogging):
command(str): subcommand to run
*args(str): positional command arguments
**kwargs(str): named command arguments
Returns:
str: spawned process id
"""
# default arguments
arguments = self.command_arguments[:]
@ -112,19 +134,36 @@ class Spawn(Thread, LazyLogging):
with self.lock:
self.active[process_id] = process
return process_id
def key_import(self, key: str, server: str | None) -> None:
def has_process(self, process_id: str) -> bool:
"""
check if given process is alive
Args:
process_id(str): process id to be checked as returned by ``Spawn._spawn_process``
Returns:
bool: True in case if process still counts as active and False otherwise
"""
with self.lock:
return process_id in self.active
def key_import(self, key: str, server: str | None) -> str:
"""
import key to service cache
Args:
key(str): key to import
server(str | None): PGP key server
Returns:
str: spawned process id
"""
kwargs = {} if server is None else {"key-server": server}
self._spawn_process("service-key-import", key, **kwargs)
return self._spawn_process("service-key-import", key, **kwargs)
def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> None:
def packages_add(self, packages: Iterable[str], username: str | None, *, now: bool) -> str:
"""
add packages
@ -132,49 +171,69 @@ class Spawn(Thread, LazyLogging):
packages(Iterable[str]): packages list to add
username(str | None): optional override of username for build process
now(bool): build packages now
Returns:
str: spawned process id
"""
# avoid abusing by building non-aur packages
kwargs = {"source": PackageSource.AUR.value, "username": username}
kwargs = {"username": username}
if now:
kwargs["now"] = ""
self._spawn_process("package-add", *packages, **kwargs)
return self._spawn_process("package-add", *packages, **kwargs)
def packages_rebuild(self, depends_on: str, username: str | None) -> None:
def packages_rebuild(self, depends_on: str, username: str | None) -> str:
"""
rebuild packages which depend on the specified package
Args:
depends_on(str): packages dependency
username(str | None): optional override of username for build process
Returns:
str: spawned process id
"""
kwargs = {"depends-on": depends_on, "username": username}
self._spawn_process("repo-rebuild", **kwargs)
return self._spawn_process("repo-rebuild", **kwargs)
def packages_remove(self, packages: Iterable[str]) -> None:
def packages_remove(self, packages: Iterable[str]) -> str:
"""
remove packages
Args:
packages(Iterable[str]): packages list to remove
"""
self._spawn_process("package-remove", *packages)
def packages_update(self, username: str | None) -> None:
Returns:
str: spawned process id
"""
return self._spawn_process("package-remove", *packages)
def packages_update(self, username: str | None, *, aur: bool, local: bool, manual: bool) -> str:
"""
run full repository update
Args:
username(str | None): optional override of username for build process
aur(bool): check for aur updates
local(bool): check for local packages updates
manual(bool): check for manual packages
Returns:
str: spawned process id
"""
kwargs = {"username": username}
self._spawn_process("repo-update", **kwargs)
kwargs = {
"username": username,
self.boolean_action_argument("aur", aur): "",
self.boolean_action_argument("local", local): "",
self.boolean_action_argument("manual", manual): "",
}
return self._spawn_process("repo-update", **kwargs)
def run(self) -> None:
"""
thread run method
"""
for process_id, status in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s", process_id, status)
for process_id, status, consumed_time in iter(self.queue.get, None):
self.logger.info("process %s has been terminated with status %s, consumed time %s",
process_id, status, consumed_time / 1000)
with self.lock:
process = self.active.pop(process_id, None)

View File

@ -24,6 +24,7 @@ import logging
from ahriman.core.configuration import Configuration
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
@ -60,7 +61,7 @@ class Client:
return WebClient(configuration)
return Client()
def add(self, package: Package, status: BuildStatusEnum) -> None:
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
@ -69,7 +70,7 @@ class Client:
status(BuildStatusEnum): current package build status
"""
def get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
get package status
@ -82,25 +83,16 @@ class Client:
del package_base
return []
def get_internal(self) -> InternalStatus:
"""
get internal service status
Returns:
InternalStatus: current internal (web) service status
"""
return InternalStatus(status=BuildStatus())
def logs(self, package_base: str, record: logging.LogRecord) -> None:
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
"""
post log record
Args:
package_base(str) package base
log_record_id(LogRecordId): log record id
record(logging.LogRecord): log record to post to api
"""
def remove(self, package_base: str) -> None:
def package_remove(self, package_base: str) -> None:
"""
remove packages from watcher
@ -108,7 +100,7 @@ class Client:
package_base(str): package base to remove
"""
def update(self, package_base: str, status: BuildStatusEnum) -> None:
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
@ -117,14 +109,6 @@ class Client:
status(BuildStatusEnum): current package build status
"""
def update_self(self, status: BuildStatusEnum) -> None:
"""
update ahriman status itself
Args:
status(BuildStatusEnum): current ahriman status
"""
def set_building(self, package_base: str) -> None:
"""
set package status to building
@ -132,7 +116,7 @@ class Client:
Args:
package_base(str): package base to update
"""
return self.update(package_base, BuildStatusEnum.Building)
return self.package_update(package_base, BuildStatusEnum.Building)
def set_failed(self, package_base: str) -> None:
"""
@ -141,7 +125,7 @@ class Client:
Args:
package_base(str): package base to update
"""
return self.update(package_base, BuildStatusEnum.Failed)
return self.package_update(package_base, BuildStatusEnum.Failed)
def set_pending(self, package_base: str) -> None:
"""
@ -150,7 +134,7 @@ class Client:
Args:
package_base(str): package base to update
"""
return self.update(package_base, BuildStatusEnum.Pending)
return self.package_update(package_base, BuildStatusEnum.Pending)
def set_success(self, package: Package) -> None:
"""
@ -159,7 +143,7 @@ class Client:
Args:
package(Package): current package properties
"""
return self.add(package, BuildStatusEnum.Success)
return self.package_add(package, BuildStatusEnum.Success)
def set_unknown(self, package: Package) -> None:
"""
@ -168,4 +152,21 @@ class Client:
Args:
package(Package): current package properties
"""
return self.add(package, BuildStatusEnum.Unknown)
return self.package_add(package, BuildStatusEnum.Unknown)
def status_get(self) -> InternalStatus:
"""
get internal service status
Returns:
InternalStatus: current internal (web) service status
"""
return InternalStatus(status=BuildStatus())
def status_update(self, status: BuildStatusEnum) -> None:
"""
update ahriman status itself
Args:
status(BuildStatusEnum): current ahriman status
"""

View File

@ -17,8 +17,6 @@
# 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 os
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.exceptions import UnknownPackageError
@ -59,7 +57,7 @@ class Watcher(LazyLogging):
self.status = BuildStatus()
# special variables for updating logs
self._last_log_record_id = LogRecordId("", os.getpid())
self._last_log_record_id = LogRecordId("", "")
@property
def packages(self) -> list[tuple[Package, BuildStatus]]:
@ -71,36 +69,6 @@ class Watcher(LazyLogging):
"""
return list(self.known.values())
def 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:
UnknownPackage: if no package found
"""
try:
return self.known[package_base]
except KeyError:
raise UnknownPackageError(package_base)
def get_logs(self, package_base: str) -> str:
"""
extract logs for the package base
Args:
package_base(str): package base
Returns:
str: package logs
"""
return self.database.logs_get(package_base)
def load(self) -> None:
"""
load packages from local repository. In case if last status is known, it will use it
@ -117,7 +85,62 @@ class Watcher(LazyLogging):
if package.base in self.known:
self.known[package.base] = (package, status)
def remove(self, package_base: str) -> None:
def logs_get(self, package_base: str) -> str:
"""
extract logs for the package base
Args:
package_base(str): package base
Returns:
str: package logs
"""
return self.database.logs_get(package_base)
def logs_remove(self, package_base: str, version: str | None) -> None:
"""
remove package related logs
Args:
package_base(str): package base
version(str): package versio
"""
self.database.logs_remove(package_base, version)
def logs_update(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
make new log record into database
Args:
log_record_id(LogRecordId): log record id
created(float): log created timestamp
record(str): log record
"""
if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones
self.logs_remove(log_record_id.package_base, log_record_id.version)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record)
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:
return self.known[package_base]
except KeyError:
raise UnknownPackageError(package_base) from None
def package_remove(self, package_base: str) -> None:
"""
remove package base from known list if any
@ -126,19 +149,9 @@ class Watcher(LazyLogging):
"""
self.known.pop(package_base, None)
self.database.package_remove(package_base)
self.remove_logs(package_base, None)
self.logs_remove(package_base, None)
def remove_logs(self, package_base: str, current_process_id: int | None) -> None:
"""
remove package related logs
Args:
package_base(str): package base
current_process_id(int | None): current process id
"""
self.database.logs_remove(package_base, current_process_id)
def update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
def package_update(self, package_base: str, status: BuildStatusEnum, package: Package | None) -> None:
"""
update package status and description
@ -148,33 +161,18 @@ class Watcher(LazyLogging):
package(Package | None): optional package description. In case if not set current properties will be used
Raises:
UnknownPackage: if no package found
UnknownPackageError: if no package found
"""
if package is None:
try:
package, _ = self.known[package_base]
except KeyError:
raise UnknownPackageError(package_base)
raise UnknownPackageError(package_base) from None
full_status = BuildStatus(status)
self.known[package_base] = (package, full_status)
self.database.package_update(package, full_status)
def update_logs(self, log_record_id: LogRecordId, created: float, record: str) -> None:
"""
make new log record into database
Args:
log_record_id(LogRecordId): log record id
created(float): log created record
record(str): log record
"""
if self._last_log_record_id != log_record_id:
# there is new log record, so we remove old ones
self.remove_logs(log_record_id.package_base, log_record_id.process_id)
self._last_log_record_id = log_record_id
self.database.logs_insert(log_record_id, created, record)
def update_self(self, status: BuildStatusEnum) -> None:
def status_update(self, status: BuildStatusEnum) -> None:
"""
update service status

View File

@ -21,28 +21,26 @@ import contextlib
import logging
import requests
from collections.abc import Generator
from functools import cached_property
from urllib.parse import quote_plus as urlencode
from ahriman import version
from ahriman import __version__
from ahriman.core.configuration import Configuration
from ahriman.core.log import LazyLogging
from ahriman.core.http import SyncHttpClient
from ahriman.core.status.client import Client
from ahriman.core.util import exception_response_text
from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus
from ahriman.models.log_record_id import LogRecordId
from ahriman.models.package import Package
from ahriman.models.user import User
class WebClient(Client, LazyLogging):
class WebClient(Client, SyncHttpClient):
"""
build status reporter web client
Attributes:
address(str): address of the web service
suppress_errors(bool): suppress logging errors (e.g. if no web server available)
user(User | None): web service user descriptor
use_unix_socket(bool): use websocket or not
"""
def __init__(self, configuration: Configuration) -> None:
@ -52,33 +50,20 @@ class WebClient(Client, LazyLogging):
Args:
configuration(Configuration): configuration instance
"""
self.address, use_unix_socket = self.parse_address(configuration)
self.user = User.from_option(
configuration.get("web", "username", fallback=None),
configuration.get("web", "password", fallback=None))
self.suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
suppress_errors = configuration.getboolean("settings", "suppress_http_log_errors", fallback=False)
SyncHttpClient.__init__(self, "web", configuration, suppress_errors=suppress_errors)
self.__session = self._create_session(use_unix_socket=use_unix_socket)
self.address, self.use_unix_socket = self.parse_address(configuration)
@property
def _login_url(self) -> str:
@cached_property
def session(self) -> requests.Session:
"""
get url for the login api
get or create session
Returns:
str: full url for web service to log in
request.Session: created session object
"""
return f"{self.address}/api/v1/login"
@property
def _status_url(self) -> str:
"""
get url for the status api
Returns:
str: full url for web service for status
"""
return f"{self.address}/api/v1/status"
return self._create_session(use_unix_socket=self.use_unix_socket)
@staticmethod
def parse_address(configuration: Configuration) -> tuple[str, bool]:
@ -102,32 +87,6 @@ class WebClient(Client, LazyLogging):
address = f"http://{host}:{port}"
return address, False
@contextlib.contextmanager
def __get_session(self, session: requests.Session | None = None) -> Generator[requests.Session, None, None]:
"""
execute request and handle exceptions
Args:
session(requests.Session | None, optional): session to be used or stored instance property otherwise
(Default value = None)
Yields:
requests.Session: session for requests
"""
try:
if session is not None:
yield session # use session from arguments
else:
yield self.__session # use instance generated session
except requests.RequestException as e:
if self.suppress_errors:
return
self.logger.exception("could not perform http request: %s", exception_response_text(e))
except Exception:
if self.suppress_errors:
return
self.logger.exception("could not perform http request")
def _create_session(self, *, use_unix_socket: bool) -> requests.Session:
"""
generate new request session
@ -141,11 +100,11 @@ class WebClient(Client, LazyLogging):
if use_unix_socket:
import requests_unixsocket # type: ignore[import]
session: requests.Session = requests_unixsocket.Session()
session.headers["User-Agent"] = f"ahriman/{version.__version__}"
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{version.__version__}"
session.headers["User-Agent"] = f"ahriman/{__version__}"
self._login(session)
return session
@ -157,17 +116,25 @@ class WebClient(Client, LazyLogging):
Args:
session(requests.Session): request session to login
"""
if self.user is None:
if self.auth is None:
return # no auth configured
username, password = self.auth
payload = {
"username": self.user.username,
"password": self.user.password
"username": username,
"password": password,
}
with contextlib.suppress(Exception):
self.make_request("POST", self._login_url(), json=payload, session=session)
with self.__get_session(session):
response = session.post(self._login_url, json=payload)
response.raise_for_status()
def _login_url(self) -> str:
"""
get url for the login api
Returns:
str: full url for web service to log in
"""
return f"{self.address}/api/v1/login"
def _logs_url(self, package_base: str) -> str:
"""
@ -195,7 +162,16 @@ class WebClient(Client, LazyLogging):
suffix = f"/{package_base}" if package_base else ""
return f"{self.address}/api/v1/packages{suffix}"
def add(self, package: Package, status: BuildStatusEnum) -> None:
def _status_url(self) -> str:
"""
get url for the status api
Returns:
str: full url for web service for status
"""
return f"{self.address}/api/v1/status"
def package_add(self, package: Package, status: BuildStatusEnum) -> None:
"""
add new package with status
@ -207,12 +183,10 @@ class WebClient(Client, LazyLogging):
"status": status.value,
"package": package.view()
}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package.base), json=payload)
with self.__get_session() as session:
response = session.post(self._package_url(package.base), json=payload)
response.raise_for_status()
def get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
def package_get(self, package_base: str | None) -> list[tuple[Package, BuildStatus]]:
"""
get package status
@ -222,66 +196,47 @@ class WebClient(Client, LazyLogging):
Returns:
list[tuple[Package, BuildStatus]]: list of current package description and status if it has been found
"""
with self.__get_session() as session:
response = session.get(self._package_url(package_base or ""))
response.raise_for_status()
with contextlib.suppress(Exception):
response = self.make_request("GET", self._package_url(package_base or ""))
response_json = response.json()
status_json = response.json()
return [
(Package.from_json(package["package"]), BuildStatus.from_json(package["status"]))
for package in status_json
for package in response_json
]
# noinspection PyUnreachableCode
return []
def get_internal(self) -> InternalStatus:
"""
get internal service status
Returns:
InternalStatus: current internal (web) service status
"""
with self.__get_session() as session:
response = session.get(self._status_url)
response.raise_for_status()
status_json = response.json()
return InternalStatus.from_json(status_json)
# noinspection PyUnreachableCode
return InternalStatus(status=BuildStatus())
def logs(self, package_base: str, record: logging.LogRecord) -> None:
def package_logs(self, log_record_id: LogRecordId, record: logging.LogRecord) -> None:
"""
post log record
Args:
package_base(str) package base
log_record_id(LogRecordId): log record id
record(logging.LogRecord): log record to post to api
"""
payload = {
"created": record.created,
"message": record.getMessage(),
"process_id": record.process,
"version": log_record_id.version,
}
# in this method exception has to be handled outside in logger handler
response = self.__session.post(self._logs_url(package_base), json=payload)
response.raise_for_status()
# this is special case, because we would like to do not suppress exception here
# in case of exception raised it will be handled by upstream HttpLogHandler
# In the other hand, we force to suppress all http logs here to avoid cyclic reporting
self.make_request("POST", self._logs_url(log_record_id.package_base), json=payload, suppress_errors=True)
def remove(self, package_base: str) -> None:
def package_remove(self, package_base: str) -> None:
"""
remove packages from watcher
Args:
package_base(str): basename to remove
"""
with self.__get_session() as session:
response = session.delete(self._package_url(package_base))
response.raise_for_status()
with contextlib.suppress(Exception):
self.make_request("DELETE", self._package_url(package_base))
def update(self, package_base: str, status: BuildStatusEnum) -> None:
def package_update(self, package_base: str, status: BuildStatusEnum) -> None:
"""
update package build status. Unlike ``add`` it does not update package properties
@ -290,12 +245,25 @@ class WebClient(Client, LazyLogging):
status(BuildStatusEnum): current package build status
"""
payload = {"status": status.value}
with contextlib.suppress(Exception):
self.make_request("POST", self._package_url(package_base), json=payload)
with self.__get_session() as session:
response = session.post(self._package_url(package_base), json=payload)
response.raise_for_status()
def status_get(self) -> InternalStatus:
"""
get internal service status
def update_self(self, status: BuildStatusEnum) -> None:
Returns:
InternalStatus: current internal (web) service status
"""
with contextlib.suppress(Exception):
response = self.make_request("GET", self._status_url())
response_json = response.json()
return InternalStatus.from_json(response_json)
return InternalStatus(status=BuildStatus())
def status_update(self, status: BuildStatusEnum) -> None:
"""
update ahriman status itself
@ -303,7 +271,5 @@ class WebClient(Client, LazyLogging):
status(BuildStatusEnum): current ahriman status
"""
payload = {"status": status.value}
with self.__get_session() as session:
response = session.post(self._status_url, json=payload)
response.raise_for_status()
with contextlib.suppress(Exception):
self.make_request("POST", self._status_url(), json=payload)

View File

@ -46,9 +46,13 @@ class KeyringTrigger(Trigger):
},
},
},
"keyring_generator": {
"keyring-generator": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["keyring-generator"],
},
"description": {
"type": "string",
},

View File

@ -42,9 +42,13 @@ class MirrorlistTrigger(Trigger):
},
},
},
"mirrorlist_generator": {
"mirrorlist-generator": {
"type": "dict",
"schema": {
"type": {
"type": "string",
"allowed": ["mirrorlist-generator"],
},
"description": {
"type": "string",
},
@ -59,8 +63,8 @@ class MirrorlistTrigger(Trigger):
"type": "string",
},
"path": {
"type": "string",
"path_is_absolute": True,
"type": "path",
"coerce": "absolute_path",
},
"servers": {
"type": "list",

View File

@ -140,6 +140,9 @@ class KeyringGenerator(PkgbuildGenerator):
Args:
source_path(Path): destination of the file content
Raises:
PkgbuildGeneratorError: no trusted keys available
"""
if not self.trusted:
raise PkgbuildGeneratorError

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